Tutorial: Compose a middleware stack
View SourceIn 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_idruns first so error responses still carry an id.wrapruns second so it catches anything below.livery_access_logruns after that so it observes the final status.livery_body_limitruns 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