[BUG] ErrorOutVariantsPrepend does not work as intended
john-joe-givery opened this issue · comments
Tapir version: 1.9.7 - 1.9.11
Scala version: 3.3.3
Describe the bug
errorOutVariantsPrepend
function will always either match on one of the error variants defined in errorOut
or throw a 500 if it cannot match any of them. It ignores any variants passed in the errorOutVariantsPrepend
function itself.
What is the problem?
errorOutVariantsPrepend
should try to match on any input error variants, and then as a backup default to any errorOut
variants that were previously defined for the endpoint.
This seems to be because the source code
How to reproduce?
import scala.concurrent.Future
import sttp.tapir.*
sealed trait ErrorInfo
case object InternalServerException extends ErrorInfo
case class UnauthorizedError(realm: String) extends ErrorInfo
val baseEndpoint = endpoint
.errorOut(
oneOf[ErrorInfo](
oneOfDefaultVariant(statusCode(StatusCode.InternalServerError).and(emptyOutputAs(InternalServerException)))
)
)
val myEndpoint = baseEndpoint
.in("test")
.errorOutVariantsPrepend(
oneOfVariant(StatusCode.Unauthorized, jsonBody[UnauthorizedError]),
oneOfVariant(StatusCode.NotFound, jsonBody[NotFoundError])
)
.serverLogic { _ => Future.successful(Left(NotFoundError("test"))) }
Trying to hit this endpoint will always return a 500 error.
This seems to be because of the way the function is written. Compare errorOutVariantsPrepend
to errorOutVariantPrepend
def errorOutVariantPrepend[E2 >: E](o: OneOfVariant[_ <: E2]): EndpointType[A, I, E2, O, R] =
withErrorOutputVariant(oneOf[E2](o, oneOfDefaultVariant(errorOutput)), identity)
/** Same as [[errorOutVariantPrepend]], but allows appending multiple variants in one go. */
def errorOutVariantsPrepend[E2 >: E](first: OneOfVariant[_ <: E2], other: OneOfVariant[_ <: E2]*): EndpointType[A, I, E2, O, R] =
withErrorOutputVariant(oneOf[E2](oneOfDefaultVariant(errorOutput), first +: other: _*), identity)
The first has oneOf(o, default)
while the second has oneOf(default, first +: other: _*)
Based on the description of oneOf
"When decoding from a response, the first output which decodes successfully is chosen.". Since the first option is a default variant in errorOutVariantsPrepend
it will always use that.
Additional information
If it's helpful, I had previously written my own errorOutVariantsPrepend
extension method which worked as expected
implicit class EndpointOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R](endpoint: Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]) {
def errorOutVariantsPrepend(first: OneOfVariant[? <: ERROR_OUTPUT], others: OneOfVariant[? <: ERROR_OUTPUT]*): Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R] =
endpoint.errorOutVariantsFromCurrent(out => (first +: others :+ oneOfDefaultVariant(out)).toList)
}
Indeed this seems incorrect ... maybe you'd like to attempt creating a PR?
It also seems we are missing tests around these functions (once again, it's good to have tests ;) ). I suppose adding them here would be the best spot, they'd then be run for all server interpreters.
I'll try to submit a PR when I have a bit of free time, hopefully later this week.