Middleware¶
A middleware is a function from a handler to a handler. It wraps every route and the not-found answer, for what applies to everything: headers, a maintenance page, timing.
Example: counting requests¶
(* A middleware of the application's own: every request is counted before
any route answers it, and the answer says how many there have been.
The count is an [Atomic] and not a [ref]: a request runs on any of the
domains the server serves on, beside the others, so a [ref] would lose
increments under load. *)
open Spindle.Syntax
let count = Atomic.make 0
let count_requests handler request =
Atomic.incr count;
handler request
let saw () = Ok (Printf.sprintf "Saw %i request(s)!" (Atomic.get count))
let routes =
[
(* [let+] over [Dep.return ()] runs [saw] on every request; [Dep.return
(saw ())] would make the answer once, when the route is. *)
Spindle.get Spindle.Path.root Spindle.Returns.text
(let+ () = Spindle.Dep.return () in
saw ());
]
let () =
Spindle.Log.setup ();
Eio_main.run @@ fun env ->
Spindle.serve env ~middleware:[ count_requests ] routes
Writing middleware¶
Two middlewares: one built in, and one that applies only to the routes that ask for it.
open Spindle.Syntax
let security =
Spindle.Middleware.headers
[ ("x-content-type-options", "nosniff"); ("referrer-policy", "same-origin") ]
(* A policy for some routes: a key a route sets, read from the route the
request matched, never from its path. *)
let noindex : bool Spindle.Meta.key = Spindle.Meta.key ()
let hide_from_search handler request =
let response = handler request in
match
Option.bind (Spindle.Route.matched request) (fun (r : Spindle.Route.info) ->
Spindle.Meta.find noindex r.meta)
with
| Some true ->
Spindle.Response.add_headers [ ("x-robots-tag", "noindex") ] response
| Some false | None -> response
let routes =
[
Spindle.get
~meta:Spindle.Meta.(empty |> add noindex true)
Spindle.Path.(s "drafts")
Spindle.Returns.text
(let+ () = Spindle.Dep.return () in
Ok "Not for search engines.");
]
let () =
Eio_main.run @@ fun env ->
Spindle.serve env ~middleware:[ security; hide_from_search ] routes
- Order:
[a; b]isa (b app): the first listed sees the request first and the response last. - It may answer alone, without calling the handler – a maintenance page,
say. Around the handler, it may add headers
(
Response.add_headers) or cookies (Response.add_cookies) to the response, or answer a response of its own instead. It cannot rewrite a stream’s body, which is written after the middleware has returned. - It cannot reroute. The request is matched before any middleware runs;
Route.matched requestis that route’sRoute.info, orNonewhen no route answered. A route puts a value there with~meta, as/draftsdoes above, and the middleware reads it withMeta.find. To send someone elsewhere, answer a redirect. - Declare its refusal codes in
~codes, onSpindle.serveorApp.make. A code nobody declared is logged as a bug, and raises in a test. These codes are not written into the OpenAPI document. - A raise is a
500, as a handler’s is. Middleware.headersis the one built in. It never overrides a header the route set itself.
What one route needs – a session, a rate limit – is not middleware but a dependency of that route, so it shows in the route and its document. Logging is the server’s own.