Current section
Files
Jump to
Current section
Files
README.md
<p align="center">
<img src="site/assets/livery-mark.svg" alt="Livery" width="96" height="96">
</p>
<h1 align="center">Livery</h1>
<p align="center">
One handler set. Every version of HTTP.
</p>
<p align="center">
<a href="https://benoitc.github.io/livery/">Website</a> ·
<a href="https://benoitc.github.io/livery/api/index.html">Docs</a> ·
<a href="docs/what-is-livery.md">What is Livery</a> ·
<a href="docs/design.md">Design</a>
</p>
---
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
```erlang
%% 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:
```erlang
Router = livery_router:compile([
{<<"GET">>, <<"/">>, fun(_Req) -> livery_resp:text(200, <<"hello">>) end}
]),
{ok, _Pid} = livery:start_service(#{http => #{port => 8080}, router => Router}).
```
```console
$ curl localhost:8080/
hello
```
Run it now: `rebar3 shell`, paste the two expressions, then `curl`.
## Route, with path params and JSON
```erlang
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.
```erlang
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:
```erlang
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:
```erlang
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.
```erlang
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
```erlang
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:
```erlang
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):
```erlang
{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`:
```erlang
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
- **One handler, three protocols**: write a handler once and serve it
over H1, H2, and H3, with the same routing and middleware, and
Alt-Svc to move clients up to H3.
- **Tower-style middleware**: `call(Req, Next, State)` pipelines over
plain values, for the whole service or for one route, in both
directions (requests coming in, and requests going out through
`livery_client`).
- **Streaming**: chunked, SSE, NDJSON, WebSocket and WebTransport
over H2/H3, and file responses with byte ranges.
- **OpenAPI**: generate a 3.2 document from routes, serve Redoc or
Swagger UI, and validate request bodies against a subset of JSON Schema.
- **MCP**: serve the Model Context Protocol Streamable HTTP transport
on the main listener.
- **A2A**: mount an Agent-to-Agent agent (card, JSON-RPC and REST
bindings, SSE streams) on the main listener.
- **Observability & auth**: OpenTelemetry-style traces and metrics,
logs tagged with the trace, JWT/JWKS/OIDC, signed sessions, token
introspection.
## Ecosystem
Companion libraries built on Livery, each in its own repo:
- **[livery_grpc](https://github.com/benoitc/livery_grpc)** - gRPC server and
client on top of Livery's HTTP/2 support: all four call types, deadlines, gRPC-Web,
server reflection, and the standard health service.
- **[livery_s3](https://github.com/benoitc/livery_s3)** - S3-compatible object
storage client on the Livery HTTP client: AWS SigV4 signing, multipart
uploads, and presigned URLs, for AWS S3, Garage, MinIO, Ceph, and Wasabi.
- **[livery_stripe](https://github.com/benoitc/livery_stripe)** - Stripe API
client on the Livery HTTP client: customers, subscriptions, Checkout, the
Billing Portal, and webhook verification.
## 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/`](docs/README.md): start with the
[Quickstart](docs/quickstart.md), then the
[tutorials](docs/README.md#tutorials) and
[how-to guides](docs/README.md#how-to-guides). To contribute, read
[CONTRIBUTING.md](CONTRIBUTING.md); to report a vulnerability, see
[SECURITY.md](SECURITY.md).
## Sponsors
<a href="https://enki-multimedia.eu"><img src="site/assets/enki-multimedia.svg" alt="Enki Multimedia" height="50" /></a>
Livery is developed and maintained by [Enki Multimedia](https://enki-multimedia.eu).
If your company relies on it, [reach out](mailto:benoitc@enki-multimedia.eu)
for sponsored support, or sponsor its maintenance via
[GitHub Sponsors](https://github.com/sponsors/benoitc).
## License
Apache-2.0.