quic_h3 (quic v1.3.0)

View Source

HTTP/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).

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.

Summary

Functions

Cancel a stream with H3_REQUEST_CANCELLED error.

Cancel a stream with a specific error code.

Cancel a push (client only).

Close the connection.

Connect to an HTTP/3 server.

Connect to an HTTP/3 server with options.

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.

Max payload we can fit in one H3 DATAGRAM for the given stream. 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).

Initiate a server push (server only).

Send an HTTP request.

Send an HTTP request with options.

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

conn/0

-type conn() :: pid().

connect_opts/0

-type connect_opts() ::
          #{cert => binary(),
            key => term(),
            cacerts => [binary()],
            verify => verify_none | verify_peer,
            settings => map(),
            quic_opts => map(),
            stream_type_handler => stream_type_handler()}.

connection_handler_fun/0

-type connection_handler_fun() :: fun((QuicConnPid :: pid()) -> per_connection_opts()).

error_code/0

-type error_code() :: non_neg_integer().

headers/0

-type headers() :: [{binary(), binary()}].

per_connection_opts/0

-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()}.

push_id/0

-type push_id() :: non_neg_integer().

server_opts/0

-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(),
            connection_handler => connection_handler_fun()}.

status/0

-type status() :: 100..599.

stream_id/0

-type stream_id() :: non_neg_integer().

stream_type_handler/0

-type stream_type_handler() :: fun((uni | bidi, stream_id(), non_neg_integer()) -> claim | ignore).

Functions

cancel(Conn, StreamId)

-spec cancel(conn(), stream_id()) -> ok.

Cancel a stream with H3_REQUEST_CANCELLED error.

cancel(Conn, StreamId, ErrorCode)

-spec cancel(conn(), stream_id(), error_code()) -> ok.

Cancel a stream with a specific error code.

cancel_push(Conn, PushId)

-spec cancel_push(conn(), push_id()) -> ok.

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.

close(Conn)

-spec close(conn()) -> ok.

Close the connection.

Immediately closes the HTTP/3 connection and underlying QUIC connection.

connect(Host, Port)

-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.

connect(Host, Port, Opts)

-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 - If true, 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.

get_peer_settings(Conn)

-spec get_peer_settings(conn()) -> map() | undefined.

Get peer HTTP/3 settings.

Returns undefined if SETTINGS has not been received yet.

get_quic_conn(Conn)

-spec get_quic_conn(conn()) -> pid().

Get the underlying QUIC connection for an H3 connection.

Needed for WebTransport which uses native QUIC streams alongside H3.

get_settings(Conn)

-spec get_settings(conn()) -> map().

Get local HTTP/3 settings.

goaway(Conn)

-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.

h3_datagrams_enabled(Conn)

-spec h3_datagrams_enabled(conn()) -> boolean().

Whether both sides negotiated RFC 9297 support and the extension is live on this connection.

max_datagram_size(Conn, StreamId)

-spec max_datagram_size(conn(), stream_id()) -> non_neg_integer().

Max payload we can fit in one H3 DATAGRAM for the given stream. Returns 0 if RFC 9297 isn't live or the peer advertised 0 for max_datagram_frame_size.

open_bidi_stream(Conn)

-spec open_bidi_stream(conn()) -> {ok, stream_id()} | {error, term()}.

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_bidi_stream(Conn, SignalType)

-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.

push(Conn, RequestStreamId, Headers)

-spec push(conn(), stream_id(), headers()) -> {ok, push_id()} | {error, term()}.

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.

request(Conn, Headers)

-spec request(conn(), headers()) -> {ok, stream_id()} | {error, term()}.

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

request(Conn, Headers, Opts)

-spec request(conn(), headers(), map()) -> {ok, stream_id()} | {error, term()}.

Send an HTTP request with options.

send_data(Conn, StreamId, Data)

-spec send_data(conn(), stream_id(), binary()) -> ok | {error, term()}.

Send body data on a request stream.

For clients, this sends request body data. For servers, this sends response body data.

send_data(Conn, StreamId, Data, Fin)

-spec send_data(conn(), stream_id(), binary(), boolean()) -> ok | {error, term()}.

Send body data with fin flag.

Set Fin to true to indicate the end of the body.

send_datagram(Conn, StreamId, Data)

-spec send_datagram(conn(), stream_id(), iodata()) -> ok | {error, term()}.

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_push_data(Conn, PushId, Data, Fin)

-spec send_push_data(conn(), push_id(), binary(), boolean()) -> ok | {error, term()}.

Send data on a push stream (server only).

Set Fin to true to indicate this is the last data.

send_push_response(Conn, PushId, Status, Headers)

-spec send_push_response(conn(), push_id(), status(), headers()) -> ok | {error, term()}.

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.

send_response(Conn, StreamId, Status, Headers)

-spec send_response(conn(), stream_id(), status(), headers()) -> ok | {error, term()}.

Send an HTTP response (server only).

Sends the response status and headers. The body should be sent separately using send_data/4.

send_trailers(Conn, StreamId, Trailers)

-spec send_trailers(conn(), stream_id(), headers()) -> ok | {error, term()}.

Send trailers on a request stream.

Trailers are sent after the body and signal the end of the stream.

set_max_push_id(Conn, MaxPushId)

-spec set_max_push_id(conn(), push_id()) -> ok | {error, term()}.

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).

set_stream_handler(Conn, StreamId, HandlerPid)

-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.

set_stream_handler(Conn, StreamId, HandlerPid, Opts)

-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.

start_server(Name, Port, Opts)

-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_server(Name)

-spec stop_server(atom()) -> ok | {error, term()}.

Stop an HTTP/3 server.

unset_stream_handler(Conn, StreamId)

-spec unset_stream_handler(conn(), stream_id()) -> ok.

Unregister a stream handler.

Future data will be sent to the connection owner.

wait_connected(Conn, Timeout)

-spec wait_connected(conn(), timeout()) -> ok | {error, timeout}.

Wait for H3 connection to be ready.

Blocks until the connection is established and SETTINGS exchanged, or until the timeout expires.