Adapters

View Source

An 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

AdapterServesBacked by
livery_test_adapterin-memoryETS, no socket
livery_h1HTTP/1.1h1
livery_h2HTTP/2h2
livery_h3HTTP/3quic (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