Skip to content

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
curl localhost:8080/
Saw 1 request(s)!
curl localhost:8080/
Saw 2 request(s)!

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] is a (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 request is that route’s Route.info, or None when no route answered. A route puts a value there with ~meta, as /drafts does above, and the middleware reads it with Meta.find. To send someone elsewhere, answer a redirect.
  • Declare its refusal codes in ~codes, on Spindle.serve or App.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.headers is 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.