servant icon indicating copy to clipboard operation
servant copied to clipboard

New combinator to return routed path in response headers

Open nbacquey opened this issue 2 years ago • 10 comments

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 the env type (as in Router 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 the Delayed 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 the WithRoutingHeader 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

nbacquey avatar Mar 14 '22 14:03 nbacquey

I would love to read a bit more on practical uses of this feature! Do you use this in tracing scenarios? Observability and Telemetry?

tchoutri avatar Mar 14 '22 14:03 tchoutri

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 avatar Mar 14 '22 16:03 nbacquey

@nbacquey This is music to my ears.

tchoutri avatar Mar 14 '22 22:03 tchoutri

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.

gdeest avatar Mar 15 '22 15:03 gdeest

I don't remember my specific usecase, but I hadn't thought about using tweakResponse like that, thanks for the tip!

nbacquey avatar Mar 15 '22 15:03 nbacquey

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?

alpmestan avatar Mar 16 '22 08:03 alpmestan

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.

nbacquey avatar Mar 16 '22 08:03 nbacquey

I rebased on the new version of #1556, which reinstates "real" type hints, instead of CaptureSingle/CaptureList

nbacquey avatar Mar 21 '22 10:03 nbacquey

Added a fix to prevent WithRoutingHeader from breaking IsElem

nbacquey avatar Apr 12 '22 12:04 nbacquey

@alpmestan @gdeest I would appreciate if we could bring this PR to a conclusion :)

tchoutri avatar Oct 27 '22 19:10 tchoutri