Tutorial: Compose a middleware stack

View Source

In this tutorial you build a middleware stack that adds a request id, logs every request, caps the body size, and catches handler crashes. It is for you once you can write a handler and want to see how work shared by many routes wraps around it. About 10 minutes.

1. Start from a handler that crashes

-module(crashy).
-export([go/1]).

go(_Req) -> error(boom).
?assertExit({boom, _},
            livery_test_adapter:run([], fun crashy:go/1, #{})).

Without middleware, the crash propagates. That is intentional: the per-request process (livery_req_proc) maps it to a 500 when a real adapter spawns the worker. In a unit test where you call the handler directly, you want a wrapping middleware to do the same.

2. Catch the crash

Wrap = livery_middleware:wrap(fun (_Class, _Reason, _Stack) ->
    livery_resp:text(500, <<"internal error">>)
end),

Cap = livery_test_adapter:run([Wrap], fun crashy:go/1, #{}),
?assertEqual(500, livery_test_adapter:status(Cap)).

livery_middleware:wrap/1 is a shorthand for try Next(Req) catch ... end. It belongs near the top of the stack.

3. Stack: outside-in

The first entry in the stack list is outermost. It sees the request first and the response last.

Stack = [
    {livery_request_id, undefined},
    livery_middleware:wrap(fun crashy:errors_to_resp/3),
    {livery_access_log, #{}},
    {livery_body_limit, #{max => 1_048_576}}
].
  • livery_request_id runs first so error responses still carry an id.
  • wrap runs second so it catches anything below.
  • livery_access_log runs after that so it observes the final status.
  • livery_body_limit runs closer to the handler so other middlewares see the request even if the body would be rejected.

4. Verify ordering with metadata

To check that middleware sees the request before the handler:

Stack = [
    livery_middleware:before(fun(R) -> livery_req:set_meta(seen, yes, R) end)
],
Handler = fun(R) ->
    Tag = livery_req:meta(seen, R),
    livery_resp:text(200, atom_to_binary(Tag))
end,
Cap = livery_test_adapter:run(Stack, Handler, #{}),
?assertEqual(<<"yes">>, livery_test_adapter:body(Cap)).

livery_middleware:before/1 turns a function that changes the request into a stack entry. after_response/1 does the same for a function that changes the response.

5. Short-circuit

A middleware that returns a response without calling Next skips the rest of the pipeline, including the handler.

NotFound = fun(_Req, _Next) -> livery_resp:text(404, <<>>) end,
Cap = livery_test_adapter:run([NotFound], fun (_) -> error(unreached) end, #{}),
?assertEqual(404, livery_test_adapter:status(Cap)).

This is how auth, rate limiting, and CORS preflight middlewares work.

Next steps

  • Reference: livery_middleware
  • Built-in modules: livery_request_id, livery_body_limit, livery_timeout, livery_access_log
  • Guide: Write a custom middleware