servant
servant copied to clipboard
New combinator to return routed path in response headers
This commit introduces a new type-level combinator, WithRoutingHeader
.
It modifies the behaviour of the following sub-API, such that all endpoints of said API return an additional routing header in their response.
A routing header is a header that specifies which endpoint the incoming request was routed to.
Endpoint are designated by their path, in which Capture'
and CaptureAll
combinators are replaced by a capture hint.
This header can be used by downstream middlewares to gather information about individual endpoints, since in most cases a routing header uniquely identifies a single endpoint.
Example:
type MyApi =
WithRoutingHeader :> "by-id" :> Capture "id" Int :> Get '[JSON] Foo
-- GET /by-id/1234 will return a response with the following header:
-- ("Servant-Routed-Path", "/by-id/<id::Int>")
To achieve this, two refactorings were necessary:
- Introduce a type
RouterEnv env
to encapsulate theenv
type (as inRouter env a
), which contains a tuple-encoded list of url pieces parsed from the incoming request. This type makes it possible to pass more information throughout the routing process, and the computation of theDelayed env c
associated with each request. - Introduce a new kind of router, which only modifies the RouterEnv, and doesn't affect the routing process otherwise:
EnvRouter (RouterEnv env -> RouterEnv env) (Router' env a)
. This new router is used when encountering theWithRoutingHeader
combinator in an API, to notify the endpoints of the sub-API that they must produce a routing header (this behaviour is disabled by default).
This PR also introduces Spec
tests for the WithRoutingHeader
combinator, which showcase some of its possible uses.
This PR is based upon #1556, and should remain WIP until it is merged.
Closes #1553
I would love to read a bit more on practical uses of this feature! Do you use this in tracing scenarios? Observability and Telemetry?
I would love to read a bit more on practical uses of this feature! Do you use this in tracing scenarios? Observability and Telemetry?
Indeed, my first motivation for this was to be able to use a middleware to observe the usage statistics and response time of the endpoints in my own API. I didn't want to use servant-ekg, because I wanted my middleware to be aware of Servant's fallback feature when capture fails ; I also preferred the routing to be done only once.
Also, I think this PR lays the groundwork for a whole new class of interesting features.
For instance, I once needed to have a portion of my API failing fast on a certain error, instead of defaulting to subsequent choices defined by :<|>
.
With this PR merged, this behavior would be really easy to implement: you'd only need a new combinator that uses an EnvRouter
, a new field in RouterEnv env
, and the appropriate switch in runChoice
@nbacquey This is music to my ears.
For instance, I once needed to have a portion of my API failing fast on a certain error, instead of defaulting to subsequent choices defined by :<|>.
I don't know what you exactly have in mind, but as a side note: it is already possible to do so to some extent.
E.g.:
data StrictMethodMatch
instance HasServer api context => HasServer (StrictMethodMatch :> api) context where
type ServerT (StrictMethodMatch :> api) m = ServerT api m
route Proxy ctx denv = tweakResponse turn405IntoFatalErrors (route (Proxy @api) ctx denv)
where turn405IntoFatalErrors (Fail e@(ServerError{errHTTPCode = 405})) = FailFatal e
turn405IntoFatalErrors response = response
hoistServerWithContext _ pctx nat = hoistServerWithContext (Proxy @api) pctx nat
This combinator captures 405 errors from a sub-API, and turns them into fatal failures instead of continuing routing. It prevents 405 errors from, e.g., being swallowed by Raw
combinators and turned into 404 errors by serveDirectoryFileServer
.
I don't remember my specific usecase, but I hadn't thought about using tweakResponse
like that, thanks for the tip!
With this PR merged, this behavior would be really easy to implement: you'd only need a new combinator that uses an EnvRouter, a new field in RouterEnv env, and the appropriate switch in runChoice
Do I understand correctly that this would require changing a type in servant and that the new combinator you describe would need to be based off a patched servant?
Do I understand correctly that this would require changing a type in servant and that the new combinator you describe would need to be based off a patched servant?
You are correct, this hypothetical new combinator would need to be based off a patched servant. My point was that the patch would be quite simple to write.
I rebased on the new version of #1556, which reinstates "real" type hints, instead of CaptureSingle
/CaptureList
Added a fix to prevent WithRoutingHeader
from breaking IsElem
@alpmestan @gdeest I would appreciate if we could bring this PR to a conclusion :)