Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@vigoo
Copy link
Contributor

@vigoo vigoo commented Jan 6, 2023

Redesigned core model

This is my second attempt to fix the core problem described in #1871 (the previous was #1882).

The primary changes

  • splitting the Http[R, E, A, B] type into Handler and Route
  • HExit can no longer represent "unhandled input"
  • the final runnable http server has to have Response as its error channel
  • middlewares are reimplemented on top of the new core types with more straightforward semantics and less generic combinators

Details

Handler and Route

Handler

The old Http[R, E, A, B] type represented something like A => ZIO[R, Option[E], B] in which it combined transformation from input to output with possibly not handling the given input ("routing") and defined powerful combinators to express complex routes and transformations with it.

In the new model we define:

trait Handler[-R, +Err, -In, +Out]

type RequestHandler[-R, +Err] = Handler[R, Err, Request, Response]
type AppHandler[-R]           = RequestHandler[R, Response]

as a possibly effectful transformation from In to Out with no possibility to not handle an input (other than failing). So Handler is basically a In => ZIO[R, Err, Out] function.

(Note that in the real implementation I'm still using HExit on both the Handler and Router level to make it possible to avoid the ZIO runtime in some cases, but that is just an optimization.)

All the constructors and operators from Http that are not are depending on partial results are implemented on Handler (so no collect, orElse, ++, etc).

Route

The remaining features of the old Http type are moved into the type Route:

trait Route[-R, +Err, -In, +Out]

type HttpRoute[-R, +Err] = Route[R, Err, Request, Response]
type App[-R]             = HttpRoute[R, Response]

Route corresponds to something like In => ZIO[R, Err, Option[Handler[R, Err, In, Out]]]: it can either evaluate to a Handler, or state that it is not handling the request, or fail, and it can do this in an effectful way if needed. Route has the "usual" zio-http constructors like collect and they can be combined with ++.

A Handler can always be converted to a Route (.toRoute). A Route can be converted to a Handler by providing a default route (.toHandler(default)).

The zio-http server itself requires App[R] which is Route[R, Response, Request, Response]. A HttpRoute[R, Err] can be converted to this using the default error reporting with route.withDefaultErrorResponse.

Implementation notes

  • Both Handler and Route is now implemented with direct executable encoding unlike the previous Http type. It was easier to experiment this way
  • HExit can no longer represent Empty (just like in my previous PR). I kept this limitation from the previous attempt to make sure Handler can never represent unhandled state. As Route still needs to represent it and I did not want to wrap everything in option, I'm representing it with HExit.succeed(null) and all methods directly returning such HExits are protected by zio.Unsafe .

Middlewares

The old "middleware" concept which was a Http => Http function is slightly modified in this model.

First of all we define HandlerAspect and RouteAspect as transformers of the two types:

trait HandlerAspect[-R, +Err, +AIn, -AOut, -BIn, +BOut]

type RequestHandlerMiddleware[-R, +Err] = HandlerAspect[R, Err, Request, Response, Request, Response]
type AppHandlerMiddleware[-R]           = RequestHandlerMiddleware[R, Response]

trait RouteAspect[-R, +Err, +AIn, -AOut, -BIn, +BOut]

type HttpRouteMiddleware[-R, +Err] = RouteAspect[R, Err, Request, Response, Request, Response]
type AppMiddleware[-R]             = HttpRouteMiddleware[R, Response]

These are equivalent to ZIOAspect and they have the same expression power:

  • they can be applyd (@@) to a handler or route
  • they can be combined with >>> (andThen)

No other generic combinators are defined on this level to keep it simple.

On top of these we reimplement all the "non-generic" middlewares as either handler or router aspects. Only those need to be router which need access to the routing itself. For example:

  • metrics, to be able to count unhandled cases
  • CORS because it adds new routes
  • dropTrailingSlash because it "doubles" the allowed routes
  • allow and allowZIO because they dynamically change the routing

Everything else (of the predefined middlewares in zio-http) turns out to be expressible as handler aspects.

To make it more approachable, most are defined using the HttpRouteMiddleware and RequestHandlerMiddleware type aliases, and most of them are implemented directly as functions on handlers, instead of composing very generic middlewares.

All these "middlewares", both handler and route aspect based, are collected to the Middleware object as before to make it more backward compatible.

Applying middlewares

The only thing remaining is to define how these middlewares are applied in a complex application, and this is where the new model must solve the reported linked issues.

I think by having this well defined, type level separation between routes and handlers the middleware application rule can be simple enough to not be surprising for users (but this is the weakest point of the whole change, of course):

With the actually used type aliases:

  • Applying a RequestHandlerMiddleware[R, E] to a RequestHandler[R, E]gives back aRequestHandler[R, E]`
  • Applying a HttpRouteMiddleware[R, E] to a HttpRoute[R, E]gives back a newHttpRoute[R, E]`
  • Applying a RequestHandlerMiddleware[R, E] to a HttpRoute[R, E]gives back a newHttpRoute[R, E]` with no change in the routing logic, and in case it routes to a handler, the given handler gets modified with the middleware.

More generally:

  • Applying a handler aspect to a Route requires that the In type is not changed by the aspect
  • The handler aspect is always applied after the routing so for example if it performs an effect, or contramap's the input, it can never affect the routing

Results

This model solves the linked issues (added unit tests for them), and it does not really reduce the power of zio-http - including effectful routing etc. that were mentioned in the linked issue.

Freedom of combining middlewares via various combinators has been limited but I did it intentionally.

We can also rename either Handler or Route back to Http but I wanted to have fresh names to make it easier to discuss the new model.

Some boilerplate (.toRoute conversions etc) could be reduced by adding more operators or implicit conversions but I'm not sure about it - and for now I wanted to keep everything as clear as possible for discussion.

All existing tests are passing.

Remaining:

  • restore fallback feature of the file/resource handlers
  • mark unsafe operators of Route
  • Restore the scaladoc
  • Write explanation into the PR
  • Update docs
  • Review implicit traces


// Http app that is accessible only via a jwt token
def user: UHttpApp = Http.collect[Request] { case Method.GET -> !! / "user" / name / "greet" =>
def user: HttpRoute[Any, Nothing] = Http.collect[Request] { case Method.GET -> !! / "user" / name / "greet" =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can keep the old type aliases to avoid the updating code??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these old aliases are conflicting with the new ones:

type HttpRoute[-R, +Err] = Http[R, Err, Request, Response]
type App[-R]             = HttpRoute[R, Response]

Probably the HttpRoute name is not very good either anymore (I defined it when it was Route[R, Err, Req, Resp]).

So we should review all the names together. I don't like much these old aliases but maybe it is more important to keep them for people who already use them.

private def plainTextApp(response: Response) =
Http.fromHExit(HExit.succeed(response)).whenPathEq(plaintextPath)
private def plainTextApp(response: Response): HttpRoute[Any, Nothing] =
Handler.response(response).toRoute.whenPathEq(plaintextPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toRoute, some old temrinology still exists??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although I think .toRoute was a nicer name than .toHttp (and Route vs Handler is a better distinction than Http vs Handler which does not make much sense to me)


import zio.http.HExit.Effect
import zio.{Cause, Trace, ZIO}
import zio.{Cause, Tag, Trace, ZEnvironment, ZIO, ZLayer}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to be able to replace this in favor of zio.Exit with appropriate optimizations applied?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened a ticket about it few days ago. Should we do it in this PR now that 2.0.6 is out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I think we would be able to simply replace it with Exit now)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed it here, with some workaround to override flatMap/map/mapBoth (also opened zio/zio#7714 but it creates stack safety issues in streams)

type RequestHandlerMiddleware[-R, +Err] = HandlerMiddleware[R, Err, Request, Response, Request, Response]
type AppHandlerMiddleware[-R] = RequestHandlerMiddleware[R, Response]

type HttpRoute[-R, +Err] = Http[R, Err, Request, Response]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vigoo Let's not use any Route terminology, and let's minimize name changes (so keep HttpApp instead of App, etc.).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old names for reference:

  type HttpApp[-R, +E] = Http[R, E, Request, Response]
  type UHttpApp        = HttpApp[Any, Nothing]
  type RHttpApp[-R]    = HttpApp[R, Throwable]
  type EHttpApp        = HttpApp[Any, Throwable]
  type UHttp[-A, +B]   = Http[Any, Nothing, A, B]

  type ResponseZIO[-R, +E]                   = ZIO[R, E, Response]
  type UMiddleware[+AIn, -BIn, -AOut, +BOut] = Middleware[Any, Nothing, AIn, BIn, AOut, BOut]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the old HttpApp is what currently is HttpRoute. If that's HttpApp we still need to call somehow the "final" thing, which fixes the Err to Response - that's what I called App[R]. Do you have a better idea?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we just don't need type aliases for that and use HttpApp[R, Response] in the code.

@jdegoes jdegoes closed this Jan 20, 2023
@jdegoes jdegoes reopened this Jan 20, 2023
@vigoo vigoo merged commit 94fe70b into main Jan 20, 2023
@vigoo vigoo deleted the redesigned-model branch January 20, 2023 10:43
@jamesward
Copy link
Contributor

One impact of this change is that previously interrupting a ZIO handing a request would close the connection, not returning a response. I was using this behavior in https://github.com/jamesward/easyracer
Now the interrupted ZIO causes an Internal Server Error response (or the default error handler) to be returned instead. Maybe that is the desired behavior, but I wanted to check.

jamesward added a commit to jamesward/easyracer that referenced this pull request Feb 8, 2023
@vigoo
Copy link
Contributor Author

vigoo commented Feb 9, 2023

Maybe that is the desired behavior, but I wanted to check.

I did not intentionally change this, but not sure if it was by design. @jdegoes what do you think?

@jdegoes
Copy link
Member

jdegoes commented Feb 19, 2023

@vigoo I think interruption maps most directly to immediate close. Can we do that in a followup?

@jamesward
Copy link
Contributor

I've filed a bug to track: #2022

@vigoo
Copy link
Contributor Author

vigoo commented Feb 27, 2023

@jamesward thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance Chore or Maintenance tasks

Projects

None yet

6 participants