-
Notifications
You must be signed in to change notification settings - Fork 411
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
Allow integrating with third-party libraries / frameworks #1988
Comments
Thanks for raising the issue @adamw! As I never had the chance to use tapir I'm not fully understanding the issue. I will check @frekw 's implementation to get a better idea but could you maybe write a specific example of the problem? A zio-http |
@vigoo Ok, maybe I'll start with our problem statement. We have a In our current implementation, the zio-http interpreter returns an Why not simply return a |
I'm sure I'm missing something, but let's consider this simple example: object Test extends ZIOAppDefault {
sealed trait Endpoint
object Endpoint {
case object A extends Endpoint
case object B extends Endpoint
def fromRequest(request: Request): ZIO[Any, Nothing, Option[Endpoint]] = { // possibly effectful
// Custom determining which endpoint to call (and decoding everything needed to do so)
ZIO.succeed {
request.url.path match {
case !! / "a" => Some(A)
case !! / "b" => Some(B)
case _ => None
}
}.debug("fromRequest")
}
def toResponse(endpoint: Endpoint): ZIO[Any, Nothing, Response] = // possibly effectful
ZIO.succeed {
endpoint match {
case A => Response.text("A")
case B => Response.text("B")
}
}.debug("toResponse")
}
val app: HttpApp[Any, Nothing] =
Http.fromOptionalHandlerZIO { request =>
Endpoint.fromRequest(request).flatMap { // effectful routing
case Some(endpoint) =>
ZIO.succeed {
Handler.fromZIO {
Endpoint.toResponse(endpoint)
}
}
case None =>
ZIO.fail(None) // not handling this request
}
}
val appWithMiddlewares = app @@ Middleware.runBefore(ZIO.debug("Before")) @@ Middleware.runAfter(ZIO.debug("After"))
def run =
appWithMiddlewares.runZIO(Request.get(URL(!! / "b"))).flatMap(_.body.asString).debug("Response: ")
} You have a potentially effectful function that takes a Once you have a match you can have any custom data prepared in that phase, and have a Now I can imagine that composing such a |
Running the above example outputs:
which is the desired sequence of evaluation in my opinion (at least that's what we tried to achieve with the 0.0.4 changes :) |
@vigoo Ah! :) In your example, routing and request handling is separate: first there's a "pick endpoint" stage, then there's a "run endpoint logic" stage. This does happen in tapir, of course, but this is all part of tapir's own "request executor". The below roughly reflects tapir's model: import zio.http._
import zio.{UIO, ZIO, ZIOAppDefault}
object Test extends ZIOAppDefault {
case class TapirRequest(r: Request)
case class TapirResponse(v: String)
object TapirInterpreter {
def apply(r: TapirRequest): UIO[Option[TapirResponse]] = ZIO.succeed {
if (r.r.path.toString().startsWith("/a")) Some(TapirResponse("A")) else None
}.debug("TapirInterpreter")
}
val app: HttpApp[Any, Nothing] =
Http.fromOptionalHandlerZIO { request =>
TapirInterpreter(TapirRequest(request)).flatMap {
case Some(value) => ZIO.succeed(Handler.succeed(Response.text(value.v)))
case None => ZIO.fail(None)
}
}
val appWithMiddlewares = app @@ Middleware.runBefore(ZIO.debug("Before")) @@ Middleware.runAfter(ZIO.debug("After"))
def run =
appWithMiddlewares.runZIO(Request.get(URL(!! / "a"))).flatMap(_.body.asString).debug("Response: ")
} output:
The |
Thanks I think I fully understand the issue now. Based on all this, it feels like what tapir generates should be a As you mentioned earlier this could be "fixed" by making handlers able to "not handle" the request, so making them a For example if you have: val app = (app1 @@ auth) ++ app2` If So adding back the That said we could simulate it by failing with a specific So I took a look at https://github.com/softwaremill/tapir/blob/b3d71468c9/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala If I understand it correctly, the problem is that Other than the above options I don't have any better idea at the moment. |
Thanks for the explanation - now the problem & your solution are much clearer :) Indeed I had the same problem in tapir, where I wanted to run the security logic only once, and only when I'm sure that the endpoint shape matches the request. This is done indeed in the I'd prefer to avoid refactoring the entire @frekw what do you think? Normally we pass the filtering as a parameter to That way we would avoid any duplicate invocations of As a side-note, maybe tapir v2 should adopt some of the ideas here, defaulting to handling the request with an endpoint if the path shape matches. Plus separating the routing & handling into separate classes so that we could integrate with play/zio-http more easily. |
@adamw I think that sounds like a very reasonable tradeoff! I may not be in the best position to judge this, as I'm not a big user outside of Caliban, but all examples I've seen using Tapir + zio-http either just lets Tapir handle everything or uses zio-http's own routing (via e.g Not sure I'm familiar enough with Tapir to make the change, but I can dig around a bit! |
@adamw Thanks for reporting this issue! ZIO HTTP has lots of happy Tapir users and we want to keep it that way, so as we discuss with @vigoo and @frekw, I just want to emphasize we will invest significantly to ensure high-quality integrations with Tapir and other libraries and frameworks remains both possible and affordable. |
@jdegoes thanks, though I think in the end there's no problem on zio-http's side, rather my lack of understanding of the design & rationale of the architecture change. Likewise ZIO users are active participants on the tapir/sttp communities so I'd like to serve them well, and I think it should be possible using the approach described above. When I get back from vacation next week I'll try to translate it to code :) |
The tapir side is now implemented, no changes required on zio-http side, so sorry for the noise, but thanks for the explanations :) |
@frekw has been working on fixing the zio-http integration with tapir (in softwaremill/tapir#2718), however due to the changes done in #1916 we have encountered some serious roadblocks.
The main problem is that in zio-http routing and handling is now separate, while tapir doesn't have such concepts. And since some middlewares are applied only after the
Router
has computed theHandler
, we can either provide an inefficient tapir integration (which is what @frekw PR implements), or an efficient, but broken one (which is undesirable).To separate routing and handling in tapir as required by zio-http, we have to perform input decoding twice. Once to check, if an endpoint matches the request. Once this is done, and we know that an endpoint matches, we run the decoding again, to actually handle the request. This is because in tapir, we decode the endpoint's inputs, and if they decode successfully, we know that the endpoint matches the request, and the decoded values are fed into the server's logic. If there's any decoding failure (such as a missing parameter, wrong arity, or any exception), the endpoint doesn't match.
Btw.: the same inefficiency is required for the Play interpreter, which unfortunately allows integrating only through a
PartialFunction[Request, Response]
. If they exposed aRequest => Option[Response]
we would be able to do that in one go. All other interpreters don't have this limitation (except zio-http and Play). This also forces us to disable some tapir functionalities (such as returningMethod Not Allowed
responses when appropriate)A possible integration point of running the handler middlewares is by providing a custom
EndpointHandler.onDecodeSuccess
method. This is called when an endpoint's input are successfully decoded (so we know that the endpoint matches), and the last handler in the chain then runs the server logic. So if we could somehow extract the middlewares and run them there, maybe we could work around the inefficiencies. But I have no idea if that's plausible. But maybe there are some better design options for zio-http which would allow us to integrate.And finally for completeness - this used to work fine with the previous versions of zio-http.
I were to frame this is a feature request, I'd say that tapir would like to be responsible for both routing & handling requests that much any of its endpoints (if no endpoints match, then tapir's interpreter returns an
Empty
handler), while still making sure that the specified middlewares/aspects are run.The text was updated successfully, but these errors were encountered: