tapir icon indicating copy to clipboard operation
tapir copied to clipboard

Cross-platform reuse blocker: OnDecodeFailureNextEndpointAttribute is server-only and private

Open ivan-klass opened this issue 1 month ago • 6 comments

We're utilizing tapir endpoint definitions for both server and client (with scala js). We've recently encountered a problem with routing, described at #141

To be honest it was (an unpleasant) surprise that if there's a path decoding mismatch, interpreter doesn't try other endpoints. IMHO it makes no sense for this not to be the default

The documented workaround suggestion is to adjust definition with special attribute to endpoint Atom input, using a onDecodeFailureNextEndpoint extension

import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.OnDecodeFailure.*

// If your codec for UserId fails, allow checking other endpoints for possible matches, like /customer/some_special_case
endpoint.in("customer" / path[UserId]("user_id").onDecodeFailureNextEndpoint)

But the problem is that this involves a server-side library.

Theoretically, I could traverse inputs of every endpoint before mounting them, and apply extension to PathCaptures, but I can't find how do it easily. Constructing a Pair back requires ParamConcat implicits which are not carried.

I think I will end up with platform-specific helper code around path[Input] that will use server library attributes at JVM and do nothing on JS, but that doesn't look pretty.

As the everyday user of the tapir, I wish I can set this in my shared endpoint-definition code. But make it a default behaviour is better! 😃

ivan-klass avatar Nov 21 '25 01:11 ivan-klass

The workaround (I wish I've avoided):

myApiCrossPlatformProject
  .settings(
     description := "Contract definitions for API between server and client"
  )
  .jvmSettings(
    // https://github.com/softwaremill/tapir/issues/4935
    libraryDependencies += Dependencies.tapirServer
  )

in .jvm/src/

import sttp.tapir.EndpointInput
import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.OnDecodeFailure

trait MyApiPlatformSpecific:
  extension [T](p: EndpointInput.PathCapture[T])
    inline def onDecodeFailureNextEndpoint: EndpointInput.PathCapture[T] =
      OnDecodeFailure.RichEndpointTransput(p).onDecodeFailureNextEndpoint

in .js/src/

import sttp.tapir.EndpointInput

trait MyApiPlatformSpecific:
  extension [T](p: EndpointInput.PathCapture[T])
    // no-op on JS, see https://github.com/softwaremill/tapir/issues/4935
    inline def onDecodeFailureNextEndpoint: EndpointInput.PathCapture[T] = p

In code - use pathSegment instead of tapir.path


package object myApi extends MyApiPlatformSpecific:
  type PathCodec[Key] = Codec[String, Key, CodecFormat.TextPlain]

  def pathSegment[T: PathCodec](templateName: String): EndpointInput.PathCapture[T] =
    path[T](templateName).onDecodeFailureNextEndpoint

ivan-klass avatar Nov 24 '25 11:11 ivan-klass

I think @lbialy had a similar problem recently. The reason why it's in the server module is because it customised server-side behavior (server path matching).

Another work-around could be to have a custom attribute, and support it in a custom DecodeFailureHandler. Currently this is handled here: https://github.com/softwaremill/tapir/blob/master/server/core/src/main/scala/sttp/tapir/server/interceptor/decodefailure/DecodeFailureHandler.scala#L117

As for changing the default - I don't think it's possible. It would be a run-time (not manifested in any way at compile-time) non-compatible change. There are use-cases for both behaviors, and you can change it globally by using a custom DFH.

The change that we might introduce in Tapir, is to move the .onDecodeFailureNextEndpoint attribute to core (or rather, deprecate the old one, and add a new one). But as I wrote, this is a bit slippery as this is a server-side only concern.

adamw avatar Nov 24 '25 11:11 adamw

@adamw I understand about runtime changes. Maybe it's possible to add an extra EndpointInput type, like PathSegment? That will only capture parts without "/", so it would be backward-compatible

ivan-klass avatar Nov 24 '25 15:11 ivan-klass

The change that we might introduce in Tapir, is to move the .onDecodeFailureNextEndpoint attribute to core (or rather, deprecate the old one, and add a new one). But as I wrote, this is a bit slippery as this is a server-side only concern.

I think the problem is more generic - an attribute of a single endpoint define the outer-world behaviour.

It looks more like a flag to applied server interpreter - how to handle url matching errors. To me it still makes no sense to abort without trying other endpoints if there's no pattern-match - all web frameworks I've used so far usually do so, and I hope tapir can align. Internally it's possible to build an url matching "tree" - i.e. once we capture a fixed prefix, we only try urls that are starting from it, so there's no much overhead in "continuing"

ivan-klass avatar Nov 24 '25 15:11 ivan-klass

@adamw if we can add a substitution attribute to core, I can help with a PR.

Could you please suggest a name / package?

ivan-klass avatar Nov 24 '25 15:11 ivan-klass

Adding a new input is a really heavyweight operation :) Wouldn't using a custom DecodeFailureHandler (customising the default one is relatively simple) work for you? If not, what's the use case? I'd like to understand the exact failing use-case, before committing to a solution.

Generally the approach in Tapir is that if the "path shape" matches, the endpoint will handle the request. At least, that's the default in the DFH. And yes, there is a prefix tree in the ServerInterpreter, which is used to select the endpoints.

adamw avatar Nov 24 '25 17:11 adamw