I’m trying to use tapir with play app and stuck with exception while testing routes. The path: /test/some-name matches the shape of some endpoint, but none of the endpoints decoded the request successfully, and the decode failure handler didn't provide a response. Play requires that if the path shape matches some endpoints, the request should be handled by tapir.
What does it mean - “Play requires that if the path shape matches some endpoints, the request should be handled by tapir.” How to handle requests by tapir if I want need to use play for hanling?
Here my routes:
val getTestEndpoint: PublicEndpoint[String, TapirError, String, Any] =
endpoint
.get
.in("test" / path[String]("name"))
.out(stringBody)
.errorOut(jsonBody[TapirError] and statusCode(StatusCode.BadRequest))
val deleteTestEndpoint: PublicEndpoint[String, TapirError, String, Any] =
endpoint
.delete
.in("test" / path[String]("name"))
.out(stringBody)
.errorOut(jsonBody[TapirError] and statusCode(StatusCode.BadRequest))
And simple logic
val getTestServerEndpoint = TestEndpoints.getTestEndpoint.serverLogic {
case (name: String) => Future.successful {
if (name.isEmpty) {
Left(TapirError(400, "test.exception", "name is blank", Instant.now()))
} else {
Right(s"Hello, $name")
}
}
}
val deleteTestServerEndpoint = TestEndpoints.deleteTestEndpoint.serverLogic {
case (name: String) => Future.successful {
if (name.isEmpty) {
Left(TapirError(400, "test.exception", "name is blank", Instant.now()))
} else {
Right(s"Goodbye, $name")
}
}
}
Test:
"test overlapping endpoints" in new App(buildApp()) {
val res = EndpointVerifier(List(WageringEndpoints.getTestEndpoint, WageringEndpoints.deleteTestEndpoint))
println(s"EndpointVerifier: $res")
val getHttpRequest = FakeRequest(GET, s"/test/some-name")
val getFuture: Future[Result] = route(app, getHttpRequest).get
val getResp = contentAsString(getFuture)
println(s"GET response: $getResp")
val deleteHttpRequest = FakeRequest(DELETE, s"/test/some-name")
val deleteFuture: Future[Result] = route(app, deleteHttpRequest).get
val deleteResp = contentAsString(deleteFuture)
println(s"DELETE response: $deleteResp")
}
Hm it seems to work fine when I’m testing with a “real” server (binding to a port and making requests using an HTTP client), so maybe it’s a matter of FakeRequest? I don’t have that on my classpath, so I can’t reproduce it just yet
Maybe I just don’t understand something and tried to compose endpoints in wrong way (with orElse). I created sample project with these two endpoints and test - GitHub - nikit-os/play-tapir-exception-example
That’s definitely an omission in the docs - I’ll try to explain this more clearly. There’s also probably a problem with single-endpoint routes, which might need fixing.
The core of the problem is that to integrate with Play, we need to provide a PartialFunction, which has two components:
isDefinedAt, which verifies that a request can be handled at all by this route
apply, which actually handles this request
In the latest tapir release we optimised the isDefinedAt invocation, so there’s no duplication of work in case a route matches (before the request was decoded twice, to check if the route can be handled). So this is faster now, but there’s a tradeoff: we assume that if there’s at least one endpoint with a matching path shape, the request will be handled by that route.
In your case, the path shape is /test/*. If you have just a GET /test/* endpoint in your route, and issue a DELETE request, the path shape matches, but the request is rejected because of a method mismatch (we don’t include the method in the test for som reasons related to being able to respond with a 405 method not allowed). Hence the exception.
Summing up, it’s not only a working solution, but also more efficient, to interpret multiple endpoints in one go. It’s even required to interpret all endpoints with the same path shape together.