Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] ClassCastException if error output is defined using oneOfDefaultVariant and a sealed trait error type #2200

Closed
rodant opened this issue Jun 8, 2022 · 0 comments · Fixed by #2221
Milestone

Comments

@rodant
Copy link

rodant commented Jun 8, 2022

Tapir version: 0.20.2

Scala version: 2.13.8

Describe the bug

I'm not sure, if it is an wrong usage of tapir by us or a real issue in the library.

When using oneOfDefaultVariant as the last variant in a OneOfVariant and having a sealed trait error type as output error, a ClassCastException gets thrown, because the returned error is tried to be cast to the default one. This doesn't happen, if oneOfDefaultVariant isn't used. If oneOfVariant is used instead, the interpreter behaves correctly. This issue was observed in our production system and can be reproduced with the provided test code.

How to reproduce?

package api.http

import api.http.ServiceEndpoint.servEndpoint
import io.circe.generic.auto._
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.must.Matchers
import sttp.capabilities.WebSockets
import sttp.client3.testing.SttpBackendStub
import sttp.client3.{ Response, UriContext }
import sttp.model.StatusCode
import sttp.model.StatusCode.InternalServerError
import sttp.tapir.client.sttp.SttpClientInterpreter
import sttp.tapir.client.sttp.WebSocketToPipe.webSocketsNotSupported
import sttp.tapir.generic.auto._
import sttp.tapir.json.circe._
import sttp.tapir.server.stub.TapirStubInterpreter
import sttp.tapir.ztapir._
import sttp.tapir.{ DecodeResult, EndpointOutput, PublicEndpoint }

import scala.concurrent.Future

class ClassCastApiDefinitionSpec extends AsyncFlatSpec with Matchers {

  it must "response with error code 404" in {
    responseWith(serverBehavior = _.thenRespondError(NotFound))
      .map(_.code mustBe StatusCode.NotFound)
  }

  it must "response with error code 500 on internal error" in {
    responseWith(serverBehavior = _.thenRespondError(ServiceInternalError()))
      .map(_.code mustBe InternalServerError)
  }

  type EndpointStub    = TapirStubInterpreter[Future, WebSockets, Unit]#TapirEndpointStub[String, ServiceFailure, User]
  type StubInterpreter = TapirStubInterpreter[Future, WebSockets, Unit]

  private def responseWith(
      serverBehavior: EndpointStub => StubInterpreter
  ): Future[Response[DecodeResult[Either[ServiceFailure, User]]]] = {
    val endpointStub = TapirStubInterpreter(SttpBackendStub.asynchronousFuture).whenEndpoint(servEndpoint)
    val stub         = serverBehavior(endpointStub).backend()
    SttpClientInterpreter()
      .toRequest(servEndpoint, Some(uri"http://test.com"))(webSocketsNotSupported[Any])("1")
      .send(stub)
  }
}

case class User(id: String, name: String)

sealed trait ServiceFailure
final case class ServiceInternalError(msg: String = "internal error") extends ServiceFailure
object NotFound extends ServiceFailure

object ServiceEndpoint {
  private val oneOfErrorVariants =
    oneOf(
      oneOfVariant(
        statusCode(StatusCode.NotFound).and(emptyOutputAs(NotFound).description("User not found"))
      ),
      oneOfDefaultVariant(
        statusCode(StatusCode.InternalServerError).and(
          jsonBody[ServiceInternalError].description("Internal server error")
        )
      )
    )
  val servEndpoint: PublicEndpoint[String, ServiceFailure, User, Any] =
    endpoint.get
      .in("users" / path[String].name("id"))
      .out(statusCode(StatusCode.Ok).and(jsonBody[User]))
      .errorOut(oneOfErrorVariants)
}

Additional information

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants