Skip to content

Move HTTP from cowboy to livery, and serve HTTP/2 and HTTP/3 - #17

Open
benoitc wants to merge 1 commit into
mainfrom
feature/livery-http
Open

benoitc wants to merge 1 commit into
mainfrom
feature/livery-http

Conversation

@benoitc

@benoitc benoitc commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Hornbeam serves HTTP with livery 0.8.0 now, not cowboy. The test suites use livery_client instead of hackney.

What changed

The listener is owned by a supervised hornbeam_listener. hornbeam:info/0 and hornbeam:is_running/0 replace the ranch introspection. hornbeam_handler is a livery handler: WSGI and ASGI responses resolve through livery_resp:stream_deferred/1 in the request process, so body chunks and Python events arrive in one mailbox. WebSocket uses the ws_handler behaviour in the new hornbeam_ws_handler.

Duplicate request headers are kept in the ASGI scope and joined with ", " in the WSGI HTTP_* keys. New max_body option, default infinity.

HTTP/2 and HTTP/3

http_version was documented but nothing read it. It selects the listeners now, default ['HTTP/1.1']:

Value Result
['HTTP/1.1'] one TCP listener, cleartext or TLS
['HTTP/1.1', 'HTTP/2'] one TLS port, protocol chosen by ALPN
['HTTP/2'] one TLS port, h2 only
plus 'HTTP/3' a QUIC listener on http3_port, announced with Alt-Svc

HTTP/2 and HTTP/3 only exist over TLS, so they fail at startup with {error, {http_version_requires_ssl, Version}} if ssl is false. It is better than serving HTTP/1.1 without saying anything.

The ALPN listener gives back what cowboy did with start_tls: a client which does not speak h2 gets HTTP/1.1 on the same port instead of a refused handshake.

For HTTP/3 the certificate and the key are converted from the PEM files to the DER form quic wants. Each listener carries its own handler state, so server_scheme and server_port describe the listener which took the request, not the service.

hornbeam:info/0 returns a list of ports per protocol, because one port can serve two protocols:

#{running => true, listeners => #{h1 => [443], h2 => [443], h3 => [443]}}

Request limits

max_request_line_size, max_header_size and max_headers were accepted and documented, but they reached nothing. They reach h1 now, which answers 414 or 431.

One bug fixed

A HEAD response over h2 or h3 killed the handler. The ASGI and WSGI producers only handled {error, closed} from a stream write, and h2/h3 answer {error, invalid_stream_state} when a body is written to a stream which must not carry one. HTTP/1.1 just drops it, so nothing showed before.

Breaking

Routes are livery entries {Method | '_', Pattern, HandlerFun | {Mod, Fun}}, not cowboy {Path, HandlerModule, Opts} tuples. Cowboy handler modules give {error, {invalid_route, _}}.

The three limit keys pass from inert to enforced, so a configuration which set them loosely applies them now. max_header_size maps on the h1 limit for one header value.

hornbeam_wsgi is removed, the build_environ/1,2 path was dead code.

Known limitation

103 Early Hints work on HTTP/1.1 and HTTP/2 but not HTTP/3, which has no interim response in livery yet. The http.response.early_hints ASGI extension is announced accordingly.

Serve HTTP through livery 0.8.0 instead of cowboy, and drive the test
suites with livery_client instead of hackney.

- The listener is owned by a new supervised hornbeam_listener;
  hornbeam:info/0 and hornbeam:is_running/0 replace ranch introspection.
- hornbeam_handler is a livery handler. WSGI and ASGI responses resolve
  through livery_resp:stream_deferred/1 in the per-request process, so
  request-body chunks and Python events share one mailbox.
- WebSocket runs on the ws_handler behaviour (new hornbeam_ws_handler);
  hornbeam_websocket keeps the upgrade entry point, scope building, and
  the session registry.
- Duplicate request headers are preserved in the ASGI scope and joined
  with ", " in WSGI HTTP_* keys.
- New max_body option (default infinity) caps the request body.

http_version selects which protocols to serve. The option was documented
but read by nothing; it now drives which listeners bind, defaulting to
['HTTP/1.1']. 'HTTP/2' and 'HTTP/3' exist only over TLS and are refused
when ssl is off rather than quietly downgraded.

- ['HTTP/1.1', 'HTTP/2'] takes livery's ALPN listener, so one TLS port
  serves both and a client that will not speak h2 still gets HTTP/1.1.
  This restores what cowboy's start_tls did.
- 'HTTP/3' adds a QUIC listener, on the bind port number by default or on
  the new http3_port, advertised through Alt-Svc. Its cert and key are
  converted from the configured PEM files to the DER shapes quic wants.
- Each listener carries its own handler state, so server_scheme and
  server_port describe the listener that took the request rather than the
  service as a whole.
- max_request_line_size, max_header_size and max_headers now reach h1,
  which answers 414 or 431 instead of ignoring them.

Fix a crash the HTTP/2 work exposed: the ASGI and WSGI producers only
handled {error, closed} from a stream write, so a HEAD response over h2 or
h3, which answers {error, invalid_stream_state} where HTTP/1.1 silently
drops the body, took the handler down.

Breaking: routes are livery router entries
{Method | '_', Pattern, HandlerFun | {Mod, Fun}} rather than cowboy
{Path, HandlerModule, Opts} tuples; cowboy handler modules are rejected.
max_header_size and friends changed from inert to enforced. The dead
hornbeam_wsgi build_environ path is removed.
@benoitc
benoitc force-pushed the feature/livery-http branch from 97bf8df to a12e0b5 Compare August 12, 2026 09:30
@benoitc benoitc changed the title Move HTTP from cowboy to livery Move HTTP from cowboy to livery, and serve HTTP/2 and HTTP/3 Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant