Adapters
View SourceAn adapter is the thin translator between a wire library and Livery's request/response model. It is the only part of Livery that knows there is a socket. Everything above it (the router, the middleware, your handlers) works in terms of request and response values, and the adapter turns those into the bytes a particular protocol expects.
Because the adapter owns so little, the same handler runs unchanged over HTTP/1.1, HTTP/2, and HTTP/3, and over an in-memory test harness with no socket at all.
When you would write one
Almost never: the four shipped adapters cover HTTP/1.1, HTTP/2, HTTP/3, and in-memory testing. You write an adapter when you want Livery's handler model over a transport it does not speak yet, for example a different HTTP implementation, an RPC over a message bus, or a bespoke test harness. If you find yourself reaching for one to add buffering, routing, or protocol logic, that work belongs upstream in the wire library or downstream in a middleware instead.
The behaviour
An adapter implements the livery_adapter behaviour:
-callback start(Name, ListenSpec, Opts) -> {ok, Listener}.
-callback stop(Listener) -> ok.
-callback send_headers(Stream, Status, Headers, SendOpts) -> SendResult.
-callback send_data(Stream, IoData, SendOpts) -> SendResult.
-callback send_trailers(Stream, Trailers) -> SendResult.
-callback reset(Stream, Reason) -> ok.
-callback peer_info(Stream) -> #{peer, tls, alpn}.
-callback capabilities(Listener) -> #{trailers, extended_connect, datagrams, capsules}.SendOpts is #{end_stream => boolean(), flush => boolean()}.
SendResult is ok | {error, closed | flow | term()}. There is an
optional send_full/5 an adapter may export to coalesce headers and body
into one write; livery:emit/3 uses it when present.
Adapters that ship
| Adapter | Serves | Backed by |
|---|---|---|
livery_test_adapter | in-memory | ETS, no socket |
livery_h1 | HTTP/1.1 | h1 |
livery_h2 | HTTP/2 | h2 |
livery_h3 | HTTP/3 | quic (quic_h3 subsystem) |
The test adapter is the one to read first: it is the smallest complete implementation, and the parity SUITE drives one handler set through every adapter to prove they behave the same.
What an adapter is not
- Not a state machine. Framing, header compression, flow control, and TLS belong to the wire library.
- Not a buffer. The body reader buffers; the adapter does not.
- Not a router. Routing happens in middleware, after the request reaches the worker.
How an adapter is wired
On a new request the adapter builds a request value, asks
livery_req_sup:start_request/1 to spawn the per-request worker, and
feeds the body in as {livery_body, Ref, _} messages:
{ok, Worker} = livery_req_sup:start_request(#{
adapter => ?MODULE, stream => Stream, req => Req,
stack => Stack, handler => Handler
}),
Worker ! {livery_body, BodyRef, {data, Chunk}},
Worker ! {livery_body, BodyRef, eof}.The worker runs the middleware and handler, then drives the response back
out through livery:emit/3, which calls your send_headers/4,
send_data/3, and send_trailers/2. So the adapter is two halves: turn
inbound wire events into the body protocol, and implement the send_*
callbacks for the outbound side.
examples/livery_example_adapter.erl is a complete, readable adapter that
does exactly this, capturing the response in ETS instead of a socket so
the wiring is easy to follow. Section 10 of
Build a complete service walks
it. To grow it into a real transport, keep the callbacks, replace the ETS
sink with socket writes, translate your wire's body events into
{livery_body, Ref, _} messages, then add a group to
test/livery_parity_SUITE.erl so it is held to the same behaviour as the
others.
Capability gating
A handler can branch on what the arriving protocol supports:
Adapter = livery_req:adapter(Req),
case Adapter:capabilities(livery_req:stream(Req)) of
#{trailers := true} ->
livery_resp:with_trailers([{<<"x-fin">>, <<"1">>}], Resp);
_ ->
Resp
end.Call capabilities/1 on the concrete adapter module the request arrived
on (livery_req:adapter/1), not on livery_adapter. trailers and
extended_connect are protocol-specific; datagrams and capsules
apply to WebTransport on H3.
Listen address
Every adapter takes the same ip => inet:ip_address() and
inet6 => boolean() listen options, translated to the wire library by
livery_inet:socket_addr_opts/1. See
Bind to an address or IPv6.
The client adapter, its dual
The same idea runs outbound. livery_client_adapter is the dual of this
behaviour: it owns the wire for an outgoing request, while the client's
layers (timeout, retry, circuit breaker) own the policy above it. The
default livery_client_hackney covers HTTP/1.1, HTTP/2, and HTTP/3. When
the target is a pool of replicas, a balance layer spreads requests
across them and livery_client_discover resolves the endpoint set. See
Make outbound HTTP requests and
Load-balance outbound requests.
See also
- Concept: Architecture
- Concept: Request lifecycle
- Tutorial: Build a complete service
- Guide: Bind to an address or IPv6
- Reference:
livery_adapter,livery_test_adapter