Livery

Livery

One handler set. Every version of HTTP.

Website · Docs · What is Livery · Design


Livery is a web framework for Erlang/OTP. It serves the same router and middleware over HTTP/1.1, HTTP/2, and HTTP/3 from one runtime. WebSocket, WebTransport, Server-Sent Events, OpenAPI, MCP, A2A, and OpenTelemetry-style observability come as modules in the same package. It follows the ideas of Rust's Axum, Tower and Hyper.

Install

%% rebar.config
{deps, [livery]}.

Hello, world

A handler takes a request value and returns a response value. Compile a router, start a service, and you are serving:

Router = livery_router:compile([
    {<<"GET">>, <<"/">>, fun(_Req) -> livery_resp:text(200, <<"hello">>) end}
]),
{ok, _Pid} = livery:start_service(#{http => #{port => 8080}, router => Router}).
$ curl localhost:8080/
hello

Run it now: rebar3 shell, paste the two expressions, then curl.

Route, with path params and JSON

show_user(Req) ->
    Id = livery_req:binding(<<"id">>, Req),
    livery_resp:json(200, json:encode(#{id => Id, name => <<"Ada">>})).

Router = livery_router:compile([
    {<<"GET">>,  <<"/users/:id">>, fun show_user/1},
    {<<"POST">>, <<"/users">>,     {users, create}}   %% {Module, Function}
]).

Stack middleware (Tower/Axum style)

A middleware is a function call(Req, Next, State): it gets the request value and Next, the rest of the pipeline, and returns a response value. Attach it to the whole service or to one route.

livery:start_service(#{
    http   => #{port => 8080},
    router => Router,
    middleware => [
        {livery_request_id, undefined},
        {livery_access_log, #{}},
        {livery_body_limit, #{max => 1048576}}
    ]
}).

One service, three protocols

The same router and middleware over H1, H2, and H3, with an Alt-Svc header that tells clients H3 is available:

livery:start_service(#{
    http   => #{port => 80},
    https  => #{port => 443, cert => Cert, key => Key},
    http3  => #{port => 443, cert => Cert, key => Key},
    router => Router,
    alt_svc => advertise
}).

Add alpn => [h2, http1] to the https map and that one TLS port serves HTTP/2 and HTTP/1.1, chosen per connection:

https => #{port => 443, cert => Cert, key => Key, alpn => [h2, http1]}

Need only one protocol? livery:start_listener(livery_h1, #{port => 8080, router => Router}).

Call other services

The client uses the same middleware model for the requests you send: wrap a request in timeouts, retries, a circuit breaker, or load balancing.

Client = livery_client:new(#{
    base_url => <<"https://api.example.com">>,
    stack    => [livery_client:timeout(5000), livery_client:retry(#{max => 3})]
}),
{ok, Resp} = livery_client:get(Client, <<"/health">>),
200 = livery_client:status(Resp).

Stream a response

events(_Req) ->
    livery_resp:sse(200, fun(Emit) ->
        Emit(#{event => <<"tick">>, data => <<"1">>}),
        Emit(#{event => <<"tick">>, data => <<"2">>})
    end).

Chunked bodies, NDJSON, file responses with byte ranges, WebSocket (over H1, H2 and H3) and WebTransport over H2/H3 work the same way.

Early-response semantics

Sometimes a handler answers before it has read the request body, like rejecting an oversized upload with a 413. On HTTP/1.1 the connection would normally close right after the response, and with a large upload still in flight the close can reach the client before it reads your 413, so the client sees a reset instead.

Livery handles this for you. When you return a full response before the whole body has arrived, it reads and throws away the rest of the request body before closing, so the client gets the response:

reject(_Req) ->
    livery_resp:json(413, [], <<"{\"error\":\"too_big\"}">>).

You limit how much it reads, and for how long, on each listener (by default there is no byte limit and a 30 s deadline):

{ok, _} = livery:start_listener(livery_h1, #{
    port => 8080,
    early_response_drain => {16#400000, 5000},  %% 4 MiB / 5 s
    handler => fun reject/1
}).

Override it for a single response, or turn it off with none:

livery_resp:json(413, [], Body, #{early_response_drain => {16#400000, 5000}}).

The per-response override works only on full responses. Streaming responses (SSE, NDJSON, chunked, files) use the listener's limit.

Features

Ecosystem

Companion libraries built on Livery, each in its own repo:

Documentation

Full guides, tutorials, and the generated API reference live at https://benoitc.github.io/livery/. The same content is in the repo under docs/: start with the Quickstart, then the tutorials and how-to guides. To contribute, read CONTRIBUTING.md; to report a vulnerability, see SECURITY.md.

Sponsors

Enki Multimedia

Livery is developed and maintained by Enki Multimedia. If your company relies on it, reach out for sponsored support, or sponsor its maintenance via GitHub Sponsors.

License

Apache-2.0.