softwaremill / tapir

Rapid development of self-documenting APIs

Home Page:https://tapir.softwaremill.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[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.