quic_h3 (quic v3.0.0)
View SourceHTTP/3 client and server API.
This module provides the public interface for HTTP/3 connections built on top of QUIC transport.
Client Usage
%% Connect to an HTTP/3 server
{ok, Conn} = quic_h3:connect("example.com", 443),
%% Send a request
Headers = [
{<<":method">>, <<"GET">>},
{<<":scheme">>, <<"https">>},
{<<":path">>, <<"/">>},
{<<":authority">>, <<"example.com">>}
],
{ok, StreamId} = quic_h3:request(Conn, Headers),
%% Receive response (async messages to owner)
receive
{quic_h3, Conn, {response, StreamId, Status, RespHeaders}} ->
io:format("Status: ~p~n", [Status])
end,
%% Close connection
ok = quic_h3:close(Conn).Server Usage
%% Start an HTTP/3 server
{ok, _} = quic_h3:start_server(my_server, 4433, #{
cert => CertDer,
key => KeyTerm,
handler => fun handle_request/5
}),
%% Handler function
handle_request(Conn, StreamId, Method, Path, Headers) ->
quic_h3:send_response(Conn, StreamId, 200, [{<<"content-type">>, <<"text/plain">>}]),
quic_h3:send_data(Conn, StreamId, <<"Hello, HTTP/3!">>, true).Responses without content
A final response to HEAD, and any 204 or 304, has no content (RFC 9110 Section 6.4.1). send_response/4 sends it at once with the end of the stream, and respond/5 sends no body. A body or trailers offered after it are dropped: send_data/4 and send_trailers/3 return ok and send nothing.
handle_request(Conn, StreamId, <<"HEAD">>, _Path, _Headers) ->
ok = quic_h3:send_response(Conn, StreamId, 200, [{<<"content-length">>, <<"1024">>}]);
handle_request(Conn, StreamId, _Method, _Path, _Headers) ->
ok = quic_h3:send_response(Conn, StreamId, 204, []).Notes:
- A 1xx never ends the stream. Send it with
send_response/4;respond/5returns{error, informational_status}. send_data/4andsend_trailers/3before a final response, with nothing or only a 1xx sent, return{error, no_final_response}(RFC 9114 Section 4.1).- Status 101 returns
{error, status_101_forbidden}: HTTP/3 has no 101 (RFC 9114 Section 4.5). - Content-Length is removed from a 1xx and a 204 (RFC 9110 Section 8.6). A 304 and a HEAD response keep it.
- Pushed responses follow the same rules with
send_push_response/4andsend_push_data/4. - As a client, DATA with content on a response without content resets the stream with H3_MESSAGE_ERROR, and none of it is delivered. A 101 received is reset the same way.
Serving HTTP/3 beside other protocols
Use accept/2 when one QUIC listener serves HTTP/3 and other protocols, told apart by ALPN. Start the listener with quic:start_server/3 and an alpn_handler, and start HTTP/3 from it for the connections that negotiated h3. Pass owner: inside the hook the caller is the QUIC connection process, which is the default.
{ok, _} = quic:start_server(my_server, 4433, #{
cert => CertDer,
key => KeyTerm,
alpn => [<<"h3">>, <<"myproto">>],
stream_reset_final_size => true,
alpn_handler => fun
(Conn, <<"h3">>) ->
quic_h3:accept(Conn, #{handler => fun handle_request/5, owner => Router});
(Conn, <<"myproto">>) ->
{ok, spawn(fun() -> myproto:loop(Conn) end)}
end
}).start_server/3 sets three QUIC options that accept/2 cannot, since the listener already runs. Set them on the listener yourself: stream_reset_final_size => true, notify_streams_available => true when you pass it to accept/2, and max_datagram_frame_size (for example 65535) when you pass h3_datagram_enabled => true.
Server Push (RFC 9114 Section 4.6)
Server push allows a server to pre-emptively send resources to a client.
Server side:
%% In request handler, push associated resources
{ok, PushId} = quic_h3:push(Conn, StreamId, [
{<<":method">>, <<"GET">>},
{<<":scheme">>, <<"https">>},
{<<":authority">>, <<"example.com">>},
{<<":path">>, <<"/style.css">>}
]),
ok = quic_h3:send_push_response(Conn, PushId, 200,
[{<<"content-type">>, <<"text/css">>}]),
ok = quic_h3:send_push_data(Conn, PushId, CssBody, true).Client side:
%% Enable push after connecting
ok = quic_h3:set_max_push_id(Conn, 10),
%% Handle push notifications
receive
{quic_h3, Conn, {push_promise, PushId, ReqStreamId, Headers}} ->
%% Server announced it will push this resource
ok;
{quic_h3, Conn, {push_response, PushId, Status, Headers}} ->
%% Push response headers received
ok;
{quic_h3, Conn, {push_data, PushId, Data, Fin}} ->
%% Push response data received
ok
end.Reset streams and their final size
A stream claimed by a stream_type_handler that the peer resets is reported as {stream_type_reset, Dir, StreamId, ErrorCode}. With the stream_reset_final_size => true option it is reported as {stream_type_reset, Dir, StreamId, ErrorCode, FinalSize} instead, FinalSize being the final size from the peer's RESET_STREAM or RESET_STREAM_AT (RFC 9000 Section 4.5). A WebTransport session needs it to count the stream's bytes against WT_MAX_DATA (draft-ietf-webtrans- http3-17 Section 5.4), since the bytes it never received count on the peer's side.
Waiting for a stream
request/2,3 returns {error, stream_limit} when the server allows no more requests at once. With the notify_streams_available => true option, the owner then receives {quic_h3, Conn, {streams_available, bidi}} once a request would open again, one message per refusal, instead of having to poll. After a GOAWAY was sent or received no new request opens, so a refused request is not told then.
The same holds for the streams the owner opens itself: open_bidi_stream/2 and open_uni_stream/2 refused with stream_limit are followed by {streams_available, bidi} or {streams_available, uni}, also after GOAWAY, since a WebTransport session keeps opening streams while it drains.
Summary
Functions
Start the server side of HTTP/3 on a QUIC connection the caller already holds, and return the HTTP/3 process.
Cancel a stream with H3_REQUEST_CANCELLED error.
Cancel a stream with a specific error code.
Cancel a push (client only).
Close the connection.
Close the connection with an HTTP/3 error code.
Connect to an HTTP/3 server.
Connect to an HTTP/3 server with options.
Return receive credit on a flow_control => manual stream.
Report whether the peer accepted 0-RTT early data.
Get peer HTTP/3 settings.
Get the underlying QUIC connection for an H3 connection.
Get local HTTP/3 settings.
Initiate graceful shutdown.
Whether both sides negotiated RFC 9297 support and the extension is live on this connection.
Largest payload one H3 DATAGRAM for the given stream can carry now: quic:datagram_max_size/1 less the quarter stream ID. Returns 0 if RFC 9297 isn't live or the peer advertised 0 for max_datagram_frame_size.
Open a client-initiated bidirectional stream outside the H3 request/response flow. Equivalent to open_bidi_stream(Conn, undefined) — the stream is a plain request stream and the caller is responsible for HEADERS/DATA framing (typically not useful on its own; prefer the /2 form below).
Open a client-initiated bidirectional stream and pre-claim it with an extension signal type (e.g. WebTransport's 16#41).
Open a unidirectional stream of an extension type (e.g. WebTransport's 16#54), write the stream-type varint and claim it.
Initiate a server push (server only).
Send an HTTP request.
Send an HTTP request with options.
Send a full HTTP response (status, headers and body) in one call.
Send body data on a request stream.
Send body data with fin flag.
Send an HTTP Datagram bound to a request stream.
Send data on a push stream (server only).
Send response headers on a push stream (server only).
Send an HTTP response (server only).
Send trailers on a request stream.
Set the maximum push ID (client only).
Register a handler to receive stream body data.
Register a handler with options.
Start an HTTP/3 server.
Stop an HTTP/3 server.
Unregister a stream handler.
Wait for H3 connection to be ready.
Types
-type accept_opts() :: #{owner => pid(), handler => fun((conn(), stream_id(), binary(), binary(), headers()) -> any()) | module(), settings => map(), stream_type_handler => stream_type_handler(), h3_datagram_enabled => boolean(), max_buffered_body => non_neg_integer(), defer_requests_until_settings => boolean(), stream_reset_final_size => boolean(), notify_streams_available => boolean()}.
-type conn() :: pid().
-type connect_opts() :: #{cert => binary(), key => term(), cacerts => [binary()], cacertfile => file:name_all(), verify => verify_none | verify_peer, settings => map(), quic_opts => map(), stream_type_handler => stream_type_handler(), stream_reset_final_size => boolean(), notify_streams_available => boolean()}.
-type connection_handler_fun() :: fun((QuicConnPid :: pid()) -> per_connection_opts()).
-type error_code() :: non_neg_integer().
-type per_connection_opts() :: #{owner => pid(), handler => fun((conn(), stream_id(), binary(), binary(), headers()) -> any()) | module(), settings => map(), stream_type_handler => stream_type_handler(), h3_datagram_enabled => boolean(), max_buffered_body => non_neg_integer(), defer_requests_until_settings => boolean(), stream_reset_final_size => boolean()}.
-type push_id() :: non_neg_integer().
-type server_opts() :: #{cert := binary(), key := term(), handler => fun((conn(), stream_id(), binary(), binary(), headers()) -> any()) | module(), settings => map(), quic_opts => map(), stream_type_handler => stream_type_handler(), max_buffered_body => non_neg_integer(), defer_requests_until_settings => boolean(), stream_reset_final_size => boolean(), notify_streams_available => boolean(), connection_handler => connection_handler_fun()}.
-type status() :: 100..599.
-type stream_id() :: non_neg_integer().
-type stream_type_handler() :: fun((uni | bidi, stream_id(), non_neg_integer()) -> claim | ignore).
Functions
-spec accept(QuicConn, Opts) -> {ok, pid()} | {error, term()} when QuicConn :: pid(), Opts :: accept_opts().
Start the server side of HTTP/3 on a QUIC connection the caller already holds, and return the HTTP/3 process.
Use it from a listener's alpn_handler to serve HTTP/3 beside other protocols on one port (see the module doc). Returning the HTTP/3 process from the hook makes it the QUIC connection's owner during the handshake, before any stream event.
Opts takes the options start_server/3 applies to each connection: handler, settings, stream_type_handler, h3_datagram_enabled, max_buffered_body, defer_requests_until_settings, stream_reset_final_size, notify_streams_available and owner. owner defaults to the caller, which inside alpn_handler is the QUIC connection process, so pass the process that should receive the owner events.
The handler can be:
- A function:
fun(Conn, StreamId, Method, Path, Headers) -> ok - A module implementing
handle_request/5
Example:
alpn_handler => fun
(Conn, <<"h3">>) ->
quic_h3:accept(Conn, #{handler => fun my_http:handle/5, owner => Router});
(Conn, <<"myproto">>) ->
{ok, spawn(fun() -> myproto:loop(Conn) end)}
endBad options return {error, {bad_opts, _}}, which fails the handshake. The QUIC options start_server/3 would set, stream_reset_final_size, notify_streams_available and max_datagram_frame_size, go on the listener.
Cancel a stream with H3_REQUEST_CANCELLED error.
-spec cancel(conn(), stream_id(), error_code()) -> ok.
Cancel a stream with a specific error code.
Cancel a push (client only).
Sends CANCEL_PUSH to tell the server we don't want this push. Can be called after receiving a push_promise notification.
-spec close(conn()) -> ok.
Close the connection.
Immediately closes the HTTP/3 connection and underlying QUIC connection.
-spec close(conn(), error_code()) -> ok.
Close the connection with an HTTP/3 error code.
Same as close/1, but the peer receives ErrorCode in CONNECTION_CLOSE, for example H3_SETTINGS_ERROR (0x109) or H3_ID_ERROR (0x108).
-spec connect(Host, Port) -> {ok, conn()} | {error, term()} when Host :: binary() | string() | inet:ip_address(), Port :: inet:port_number().
Connect to an HTTP/3 server.
Establishes a QUIC connection with ALPN "h3" and starts the HTTP/3 connection layer.
The calling process becomes the owner and will receive HTTP/3 events as messages.
-spec connect(Host, Port, Opts) -> {ok, conn()} | {error, term()} when Host :: binary() | string() | inet:ip_address(), Port :: inet:port_number(), Opts :: connect_opts().
Connect to an HTTP/3 server with options.
Options:
sync- Iftrue, wait for H3 connection to be established before returning. This ensures requests can be made immediately. Default:false.connect_timeout- Timeout in ms for sync connect. Default: 5000.
-spec consume(conn(), stream_id(), non_neg_integer()) -> ok | {error, term()}.
Return receive credit on a flow_control => manual stream.
Call it after processing Bytes of body received on StreamId; the peer may then send up to one stream window past what was consumed. HTTP/3 framing is credited without it, so count payload bytes only. A no-op returning ok on an auto stream. Returns {error, unknown_stream} once the stream is finished or reset.
ok = quic_h3:set_stream_handler(Conn, StreamId, self(), #{flow_control => manual}),
%% after handling Data:
ok = quic_h3:consume(Conn, StreamId, byte_size(Data)).
Report whether the peer accepted 0-RTT early data.
Returns true if the server accepted 0-RTT, false if it rejected the early data, or unknown before the handshake reaches a state where the outcome is known. RFC 9001 Section 4.6.
Get peer HTTP/3 settings.
Returns undefined if SETTINGS has not been received yet.
Get the underlying QUIC connection for an H3 connection.
Needed for WebTransport which uses native QUIC streams alongside H3.
Get local HTTP/3 settings.
-spec goaway(conn()) -> ok.
Initiate graceful shutdown.
Sends a GOAWAY frame to the peer. No new requests will be accepted, but existing streams will complete.
Whether both sides negotiated RFC 9297 support and the extension is live on this connection.
-spec max_datagram_size(conn(), stream_id()) -> non_neg_integer().
Largest payload one H3 DATAGRAM for the given stream can carry now: quic:datagram_max_size/1 less the quarter stream ID. Returns 0 if RFC 9297 isn't live or the peer advertised 0 for max_datagram_frame_size.
Open a client-initiated bidirectional stream outside the H3 request/response flow. Equivalent to open_bidi_stream(Conn, undefined) — the stream is a plain request stream and the caller is responsible for HEADERS/DATA framing (typically not useful on its own; prefer the /2 form below).
-spec open_bidi_stream(conn(), non_neg_integer() | undefined) -> {ok, stream_id()} | {error, term()}.
Open a client-initiated bidirectional stream and pre-claim it with an extension signal type (e.g. WebTransport's 16#41).
When SignalType is a non-negative integer, the stream is recorded in the H3 connection's claimed-bidi table. Subsequent inbound data on that stream bypasses the HTTP/3 request parser and is delivered to the connection owner as {quic_h3, Conn, {stream_type_data, bidi, StreamId, Data, Fin}}. The owner also receives {quic_h3, Conn, {stream_type_open, bidi, StreamId, SignalType}} at open time, mirroring the peer-initiated claimed-bidi path.
The caller sends the extension signal varint (and any session/header prefix) itself via send_data/4 — this API is extension-agnostic.
When SignalType is undefined, no claim is recorded and the stream behaves as a normal H3 request stream.
-spec open_uni_stream(conn(), non_neg_integer()) -> {ok, stream_id()} | {error, term()}.
Open a unidirectional stream of an extension type (e.g. WebTransport's 16#54), write the stream-type varint and claim it.
The owner receives {quic_h3, Conn, {stream_type_open, uni, StreamId, StreamType}}, and later {stream_type_stop_sending, uni, StreamId, ErrorCode} if the peer stops the stream, as for a stream it opened. Write the rest of the stream with quic:send_data/4 on get_quic_conn/1.
Initiate a server push (server only).
Sends a PUSH_PROMISE on the request stream and allocates a push ID. Returns the push ID for subsequent send_push_response/send_push_data calls.
The Headers should contain the pseudo-headers for the pushed request: :method, :scheme, :authority, and :path.
Send an HTTP request.
Opens a new request stream and sends the HEADERS frame. Returns the stream ID for tracking the response.
Required pseudo-headers:
:method- HTTP method (GET, POST, etc.):scheme- URL scheme (https):path- Request path:authority- Host authority
Send an HTTP request with options.
Options:
end_stream- Close the request side after the headers (defaulttrue).flow_control-auto(default) ormanual. Withmanualthe response body waits forconsume/3: the server can send at most one stream window past what the caller consumed.
Send a full HTTP response (status, headers and body) in one call.
Coalesces the work of send_response/4 plus a final send_data/4 with end-stream. HEAD, 204 and 304 responses send no body. A 1xx returns {error, informational_status} and 101 {error, status_101_forbidden}.
Send body data on a request stream.
For clients, this sends request body data. For servers, this sends response body data: after a HEAD, 204 or 304 response it returns ok and sends nothing, and before a final response it returns {error, no_final_response}.
-spec send_data(conn(), stream_id(), binary(), boolean()) -> ok | {error, send_queue_full | term()}.
Send body data with fin flag.
Set Fin to true to indicate the end of the body. A server gets ok with nothing sent after a HEAD, 204 or 304 response, and {error, no_final_response} before a final response.
Returns {error, send_queue_full} when the connection's send queue is full. Nothing was written; wait for the connection to drain and send the same piece again. Once the queue is below half its ceiling, the caller receives {quic_h3, Conn, {send_ready, StreamId}}, once.
Send an HTTP Datagram bound to a request stream.
Enabled by passing h3_datagram_enabled => true to connect/3 or start_server/3; the underlying QUIC connection must also advertise a non-zero max_datagram_frame_size (RFC 9221) for the H3 extension to go live.
Returns {error, h3_datagrams_disabled} when the extension was not negotiated, {error, unknown_stream} when the stream id is not a known request stream, or one of the RFC 9221 error atoms (datagrams_not_supported, datagram_too_large, datagram_too_large_for_path, congestion_limited) forwarded from the QUIC layer.
Payload for a WebTransport-style CONNECT session is typically handed over unmodified; the quarter-stream-id prefix is added automatically.
Send data on a push stream (server only).
Set Fin to true to indicate this is the last data. Returns ok and sends nothing after a HEAD, 204 or 304 pushed response, and {error, no_final_response} before a final one.
Send response headers on a push stream (server only).
After push/3 returns a push ID, use this to send the response headers. The :status pseudo-header is added automatically. A pushed response to HEAD, or a 204 or 304, ends the push stream. Status 101 returns {error, status_101_forbidden}.
Send an HTTP response (server only).
Sends the response status and headers. The body should be sent separately using send_data/4. A HEAD, 204 or 304 response is sent at once and ends the stream. A 1xx is interim and can be followed by more. Content-Length is removed from a 1xx and a 204. Status 101 returns {error, status_101_forbidden}.
Send trailers on a request stream.
Trailers are sent after the body and signal the end of the stream. A server gets ok with nothing sent after a HEAD, 204 or 304 response, and {error, no_final_response} before a final response.
Set the maximum push ID (client only).
This enables server push up to the specified push ID. Call this after connecting to allow the server to push resources. The MaxPushId cannot be decreased once set.
Example:
%% Enable push with up to 10 promised resources (push IDs 0-9)
ok = quic_h3:set_max_push_id(Conn, 9).
-spec set_stream_handler(conn(), stream_id(), pid()) -> ok | {ok, [{binary(), boolean()}]} | {error, term()}.
Register a handler to receive stream body data.
By default, body data messages are sent to the connection owner. For server handlers that need to receive body data (e.g., POST bodies), call this function to redirect data to the handler process.
The handler will receive messages of the form: {quic_h3, Conn, {data, StreamId, Data, Fin}}
If data arrived before registration, it is returned as a list of {Data, Fin} tuples that the handler should process.
Example:
handle_request(Conn, StreamId, <<"POST">>, _Path, _Headers) ->
case quic_h3:set_stream_handler(Conn, StreamId, self()) of
ok ->
receive_body(Conn, StreamId, <<>>);
{ok, BufferedChunks} ->
Body = process_chunks(BufferedChunks),
receive_body(Conn, StreamId, Body)
end.
-spec set_stream_handler(conn(), stream_id(), pid(), map()) -> ok | {ok, [{binary(), boolean()}]} | {error, term()}.
Register a handler with options.
Options:
drain_buffer- If true (default), returns buffered data. If false, sends buffered data as messages.flow_control-auto(default) grants the peer receive credit as data arrives.manualgrants it only for the bytes the handler returns withconsume/3, so a handler that stops reading stops the peer after one stream window. Data buffered before registration counts as received and is consumed like any other. Without the option the stream keeps its current mode.
-spec start_server(Name, Port, Opts) -> {ok, pid()} | {error, term()} when Name :: atom(), Port :: inet:port_number(), Opts :: server_opts().
Start an HTTP/3 server.
The server listens on the given port and accepts HTTP/3 connections. Each incoming request triggers the handler with request details.
The handler can be:
- A function:
fun(Conn, StreamId, Method, Path, Headers) -> ok - A module implementing
handle_request/5
Example:
{ok, _} = quic_h3:start_server(my_server, 4433, #{
cert => CertDer,
key => KeyTerm,
handler => fun(Conn, StreamId, <<"GET">>, Path, _) ->
Body = <<"Hello from ", Path/binary>>,
quic_h3:send_response(Conn, StreamId, 200, []),
quic_h3:send_data(Conn, StreamId, Body, true)
end
}).Receiving datagrams / stream-type events: when h3_datagram_enabled or stream_type_handler is used, owner-addressed messages are emitted per connection. Supply a durable receiver by returning #{owner => Pid} from the per-connection connection_handler callback. Without an explicit owner those messages route to the listener process, where they are discarded.
Stop an HTTP/3 server.
Unregister a stream handler.
Future data will be sent to the connection owner.
Wait for H3 connection to be ready.
Blocks until the connection is established and SETTINGS exchanged, until the QUIC connection fails (the failure reason is returned, e.g. {certificate_invalid, _}), or until the timeout expires.