1

I have some tapir endpoints defined as part of a zio-http server. Everything works for real, including those POST endpoints with JSON bodies.

However, I've been unable to get a unit test using SttpBackendStub & TapirStubInterpreter to work for endpoints with a JSON body - if I remove the body from the endpoint and test request, it works fine. With the body in place, I get a 404, with no other error info.

The endpoint is defined like:

  case class Payload(one: String, two: String)

  @endpointInput("accounts/{id}/test-request")
  final case class RequestInput(
      @path
      id: String,
      @header("Source-Event-Timestamp")
      sourceEventTimestamp: LocalDateTime,
      @header("Accept")
      accept: String,
      @jsonbody
      payload: Payload
  )  

  val input = EndpointInput.derived[RequestInput]
  baseEndpoint.post.in(input)

I also experimented with using

.in(jsonBody[Input])

along with corresponding zio-json encoder & decoder, which surprisingly results in a different failure - a 400 with the message "Invalid value for: body."

Here is the relevant test code snippet (this is a ZIO spec):

        stub = TapirStubInterpreter(SttpBackendStub(new RIOMonadAsyncError[Any]))
          .whenServerEndpoint(endpoint)
          .thenRunLogic()
          .backend()
        body =
          """
            {
              "one": "",
              "two": ""
            }
          """.stripMargin
        response <- basicRequest
          .contentType("application/json")
          .body(body)
          .post(uri"http://test.com/test-request")
          .send(stub)

I've tried every permutation of calls/methods I can think of and just can't get the test to work for an endpoint with a body - again, bear in mind the real server does work with the exact same JSON body input.

  • How is `MappingRequestInput` defined? – Gaël J May 23 '23 at 19:22
  • That should've been "Input." Corrected. – Bender Rodriguez May 23 '23 at 19:30
  • I'm not familiar with sttp stub. I guess if I had to troubleshoot, I'd set some breakpoints in the code that generates the http 400 error (either in Tapir itself or maybe in a custom error handler that you'd add) and see the root cause of the http 400. – Gaël J May 24 '23 at 17:55

1 Answers1

1

I think there's something missing from your example. I tried reproducing the problem, using the following code:

import sttp.tapir.EndpointIO.annotations.jsonbody
import sttp.tapir.ztapir._
import sttp.client3._
import sttp.client3.impl.zio.RIOMonadAsyncError
import sttp.client3.testing.SttpBackendStub
import sttp.tapir.{EndpointInput, Schema}
import sttp.tapir.server.stub.TapirStubInterpreter
import sttp.tapir.json.zio._
import zio.{Console, ZIO, ZIOAppDefault}
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}

object TestWithJsonBodyUsingZioJson extends ZIOAppDefault {
  case class Payload(one: String, two: String)
  implicit val encoder: JsonEncoder[Payload] = DeriveJsonEncoder.gen[Payload]
  implicit val decoder: JsonDecoder[Payload] = DeriveJsonDecoder.gen[Payload]
  implicit val schema: Schema[Payload] = Schema.derived[Payload]

  case class RequestInput(@jsonbody payload: Payload)

  val input = EndpointInput.derived[RequestInput]
  val myEndpoint = endpoint.post.in(input).out(stringBody).zServerLogic(r => ZIO.succeed(s"Got request: $r"))

  val stub = TapirStubInterpreter(SttpBackendStub(new RIOMonadAsyncError[Any]))
    .whenServerEndpoint(myEndpoint)
    .thenRunLogic()
    .backend()
  val body = """
              {
                "one": "",
                "two": ""
              }
            """.stripMargin
  val response = basicRequest
    .contentType("application/json")
    .body(body)
    .post(uri"http://test.com/test-request")
    .send(stub)

  override def run = response.flatMap { r =>
    Console.printLine(r.toString())
  }
}

And I'm getting the expected result:

Response(Right(Got request: RequestInput(Payload(,))),200,,Vector(Content-Type: text/plain; charset=UTF-8),List(),RequestMetadata(POST,http://test.com/test-request,Vector(Accept-Encoding: gzip, deflate, Content-Type: application/json, Content-Length: 98)))

Maybe you can try to create a reproducible example?

adamw
  • 8,038
  • 4
  • 28
  • 32
  • So sorry; I munged the example in converting it to something non-proprietary. I am indeed using EndpointInput.derived as intended. Fixed the question text. – Bender Rodriguez May 29 '23 at 20:51
  • I switched to using HttpClientZioBackend in the spec, same result. I have code that works using Scala 2 and corresponding versions of all the sttp/tapir/zio libs; the Scala 3 code, using different versions (of necessity), does not work. I'm working on narrowing down the specifics. Thank you for responding here. – Bender Rodriguez May 29 '23 at 20:52
  • I've updated the answer with some example code that I think works properly. Btw. for general troubleshooting like the above I think https://softwaremill.community might be a better place, rather than SO (which is more of a Q&A site) – adamw May 30 '23 at 10:15
  • Your example code does not compile under Scala 3. I've run into this Dotty issue before, including with this code, which is part of the reason it is structured as it is. Here is the error: – Bender Rodriguez May 30 '23 at 19:29
  • [error] -- [E007] Type Mismatch Error: /Users/ehq178/dev/pfc-card-dynamic-mapper/server/src/test/scala/com/capitalone/prometheus/Temp.scala:24:24 [error] 24 | .whenServerEndpoint(myEndpoint) [error] | ^^^^^^^^^^ [error] |Found: (TestWithJsonBodyUsingZioJson.myEndpoint : [error] | sttp.tapir.ztapir.ZServerEndpoint[Nothing, Any] [error] |) [error] |Required: sttp.tapir.server.ServerEndpoint.Full[A, U, I, E, O, Nothing, [error] | [_] =>> zio.RIO[Any, _] [error] |] – Bender Rodriguez May 30 '23 at 19:29
  • [error] | [error] |where: A is a type variable with constraint [error] | E is a type variable with constraint [error] | I is a type variable with constraint [error] | O is a type variable with constraint [error] | U is a type variable with constraint – Bender Rodriguez May 30 '23 at 19:29
  • SO comment formatting - sheesh. I'm trying to get around this issue. Thanks for the sample code. – Bender Rodriguez May 30 '23 at 19:30
  • Typing this explicitly to _root_.sttp.tapir.ztapir.ZServerEndpoint[Any, Any] makes Dotty happy. – Bender Rodriguez May 31 '23 at 15:13