Mixing oneOf Variants with Exception Handler

Hello,

We’re migrating some legacy services which have a bunch of http4s exception handlers, which I’ve been able to reproduce by configuring the handler in the Tapir http4s interpreter which has been great.

We’re now starting to move some endpoints over to using the oneOf variant matchers, but this comes with the problem that oneOf needs to be exhaustive, but I want to be able to handle a subset of the errors in Tapir (and generate the docs) but continue falling back for any unmapped errors.

The exception mapper is invoked, but with a Tapir exception complaining about non-exhaustive matches:

ExceptionContext(java.lang.IllegalArgumentException: None of the mappings defined in the one-of output: one of({body as application/json (UTF-8)} status code (401)|status code (403) {body as application/json (UTF-8)}), is applicable to the value: my.custom.Error$: bla. Verify that the type parameters to oneOf are correct, and that the oneOfVariants are exhaustive (that is, that they cover all possible 

This is how we define the endpoints:

    Route.authenticated
      .in("some" / "route")
      .in(jsonBody[ReqBodyObj])
      .out(jsonBody[ResBodyObj])
      // Route.authenticated specifies errorOut as 'AuthenticationError'
      // Now, we specialise the error output
      .errorOutVariant(oneOfVariant(statusCode(StatusCode.Forbidden).and(jsonBody[SpecialisedError])))
      .serverLogicRecoverErrors { user => input =>
      // Raise SomeRandomError that should be mapped without variants

Any way to subvert this would be greatly appreciated. It’s too large of an undertaking for me to convert every error in the exception mapper to oneOf variants at the moment :confused:

Hm interesting use case - that’s not something I think we took into account :wink:

But maybe the following will work, it is however a slight hack:

case class MyException1(x: String) extends Exception

baseEndpoint.get.errorOut(
  oneOf(
    oneOfVariant[MyException1](statusCode(StatusCode.Forbidden).and(stringBody.mapTo[MyException1])),
    oneOfDefaultVariant(stringBody.map(_ => new Exception())(e => throw e))
  )
)

The first case is your specific error mappings that are defined for the endpoint. The second is the catch-all, which just pretends to be a real output. In fact, what it does during encoding of the value (the second map function, here e => throw e) is instead of converting to the lower-level representation, it just throws it. Which should end up in the exception handler.

You can also achieve a similar result with a base endpoint & .errorOutVariantPrepend for the specific mappings:

val baseEndpoint = endpoint.in(path[String]("x")).errorOut(stringBody.map(_ => new Exception())(e => throw e))

val specialisedEndpoint =
  baseEndpoint.get.errorOutVariantPrepend(
    oneOfVariant[MyException1](statusCode(StatusCode.Forbidden).and(stringBody.mapTo[MyException1]))
  )
1 Like

That worked perfectly. Hopefully just a temporary use case :wink: thank you for such a prompt reply!

1 Like