nhttp_h2 (nhttp_lib v1.2.1)
View SourceHTTP/2 protocol layer.
This module implements RFC 9113 HTTP/2 connection and stream state machines. It sits between the framing layer (nhttp_h2_frame) and the application layer, providing:
- Connection lifecycle management (preface, settings, shutdown)
- Stream state machine (RFC 9113 Section 5.1)
- Flow control (connection and stream level)
- Header block assembly (CONTINUATION handling)
- Error handling (connection vs stream errors)
- Stream turnover accounting (RFC 9113 Section 10.5)
Usage
Conn0 = nhttp_h2:new(client),
Preface = nhttp_h2:preface(Conn0),
ok = ssl:send(Socket, Preface),
{ok, Events, Conn1} = nhttp_h2:recv(Conn0, Data),
lists:foreach(fun handle_event/1, Events),
{ok, Conn2, Frames} = nhttp_h2:send_headers(Conn1, StreamId, Headers, fin),
ok = ssl:send(Socket, Frames).Sending bodies and trailers
The HTTP/2 send surface is frame-oriented. The canonical
nhttp_lib:request/0 and nhttp_lib:response/0 maps can carry a body
and trailers field as a convenience for "everything is in memory", but
this layer does not consume those maps directly: the caller breaks the
exchange into discrete frame sends. The pattern is:
send_headers(Conn, StreamId, Headers, nofin)to emit pseudo-headers plus regular headers (HEADERS + optional CONTINUATION).- Zero or more
send_data(Conn, StreamId, Chunk, nofin)calls. - Either
send_data(Conn, StreamId, FinalChunk, fin)to close on body, orsend_headers(Conn, StreamId, Trailers, fin)to close on trailers.
For a one-shot send when the body is already a single iodata(), call
send_headers/4 with nofin followed by send_data/4 with fin.
Flow-control and END_STREAM ride on the DATA frame.
Send queue
With send_queue => true, send_data/4 holds what the credit cannot
take and answers {queued, Conn, Frames, Buffered}. recv/2 emits the
held octets as credit arrives, after its control frames, round-robin
with one frame per stream per turn, and reports
{data_sent, StreamId, Bytes, Fin} per stream. The caller MUST write
every iodata the codec returns, in order, from one process.
max_send_buffer (128 KiB) and max_queued_streams (a quarter of the
peer SETTINGS_MAX_CONCURRENT_STREAMS) refuse an offer as
{error, send_buffer_full}. RST_STREAM in either direction and GOAWAY
purge the queue without restoring credit. A queued fin leaves the
stream open until the frame goes out, and send_headers/4 behind
queued data answers {error, {data_pending, StreamId}}.
Summary
Functions
Return the connection send window in octets.
Return the number of live entries in the dynamic table of the HPACK encoder.
Return the size of the dynamic table of the HPACK encoder in octets.
Create a new HTTP/2 connection with default settings.
Create a new HTTP/2 connection with custom settings.
Open a new stream and return its ID.
Return the settings the peer sent, merged over the defaults.
Generate the connection preface for this role. Client sends: magic + SETTINGS. Server sends: SETTINGS.
Return the number of streams with an entry in the send queue.
Process incoming data and return events.
Return the octets the send queue holds on the connection.
Return the octets the send queue holds for one stream, 0 without an entry.
Send every DATA frame that the current credit allows.
Send GOAWAY to initiate graceful shutdown.
Send HEADERS frame for a new request/response or trailers.
Send PING frame with caller-supplied 8-byte opaque data. The caller is
responsible for generating the opaque value (e.g. via
crypto:strong_rand_bytes(8)) and matching it against the PING_ACK event.
Send RST_STREAM to cancel a stream.
Send WINDOW_UPDATE for the connection or for a stream.
Record the peer address on the connection. Called once after the socket is
accepted (or connected) so server-built nhttp_lib:request/0 maps and
client-built nhttp_lib:response/0 events carry the correct remote peer.
Return the send window of one stream in octets.
Return the stream counters for this connection.
active is the number of streams in the "open" state or in either
"half-closed" state, the set that SETTINGS_MAX_CONCURRENT_STREAMS bounds
(RFC 9113 Section 5.1.2). peer_opened is the total number of streams that
the peer opened. peer_reset is the number of streams that the peer
terminated with RST_STREAM.
SETTINGS_MAX_CONCURRENT_STREAMS bounds the streams that are open at one
instant. It does not bound stream turnover. A peer that alternates HEADERS
and RST_STREAM holds active at a low value and drives peer_opened and
peer_reset without limit. RFC 9113 Section 10.5 tells an implementation to
track such use and to set a limit on it.
This library holds no clock, so it counts events only. The caller reads these
counters to apply a rate per unit of time. To let the connection refuse the
peer on its own, set max_reset_streams in the local settings. The
connection then fails with a connection error of type ENHANCE_YOUR_CALM when
peer_reset is more than max_reset_streams + (peer_opened div 2). The
default is infinity, which counts without a limit.
Types
-opaque conn()
-type error_code() :: nhttp_lib:error_code().
-type event() :: nhttp_lib:event_common() | {stream_closed, nhttp_lib:stream_id(), error_code()} | {stream_refused, nhttp_lib:stream_id()} | {window_update, nhttp_lib:stream_id(), pos_integer()} | {data_sent, nhttp_lib:stream_id(), non_neg_integer(), fin()} | {settings, settings()} | settings_ack | {ping, binary()} | {ping_ack, binary()}.
Events that recv/2 reports.
A window_update event mirrors one WINDOWUPDATE frame from the peer, with
stream id 0 for the connection window. A SETTINGS frame that changes
initial_window_size adjusts every stream send window inside the codec and
reports `{settings, }only.connection_send_window/1andstream_send_window/2` read the adjusted windows.
A data_sent event reports the octets the send queue emitted for a
stream in this call, with Fin fin when the END_STREAM frame went out.
-type fin() :: nhttp_lib:fin().
-type priority() :: #{exclusive := boolean(), stream_dependency := nhttp_lib:stream_id(), weight := 1..256}.
-type role() :: nhttp_lib:role().
-type send_error() :: connection_closing | send_buffer_full | {data_pending, nhttp_lib:stream_id()} | {unknown_stream, nhttp_lib:stream_id()} | {stream_closed, nhttp_lib:stream_id()} | {stream_error, nhttp_lib:stream_id(), error_code(), binary()} | {recv_window_overflow, nhttp_lib:stream_id() | connection}.
Reasons a send function refuses a call.
{recv_window_overflow, Target} reports that the increment given to
send_window_update/3 takes the local receive window of Target past
2^31-1 (RFC 9113 Section 6.9.1). The codec sent nothing, the peer saw
nothing, and the connection stays open.
send_buffer_full reports an offer past max_send_buffer or
max_queued_streams, and {data_pending, StreamId} a HEADERS frame on a
stream with octets in the send queue. Nothing changed in either case.
-type settings() :: #{header_table_size => non_neg_integer(), enable_push => boolean(), max_concurrent_streams => pos_integer() | infinity, initial_window_size => 1..2147483647, max_frame_size => 16384..16777215, max_header_list_size => pos_integer() | infinity, max_continuation_frames => pos_integer() | infinity, enable_connect_protocol => boolean(), max_reset_streams => pos_integer() | infinity, hpack_index_policy => nhttp_hpack:index_policy(), send_queue => boolean(), max_send_buffer => pos_integer() | infinity, max_queued_streams => pos_integer() | infinity}.
Connection settings.
Most keys map to a SETTINGS parameter of RFC 9113 Section 6.5.2 and go on
the wire. max_continuation_frames, max_reset_streams,
hpack_index_policy, send_queue, max_send_buffer and
max_queued_streams are local policy. They have no wire representation,
and the encoder drops them. The first two bound work that a peer can
request. hpack_index_policy reaches nhttp_hpack:new/2 and names the
fields that the HPACK encoder keeps out of its dynamic table. The
nhttp_hpack module documentation gives the two literal forms and the
sets that fit a deployment. send_queue, max_send_buffer and
max_queued_streams configure the send queue of the module
documentation.
max_continuation_frames bounds the number of CONTINUATION frames in one
field section, per RFC 9113 Section 10.5. An empty CONTINUATION frame adds
no bytes, so max_header_list_size alone does not bound it. When the key
is absent, the bound is
ceil(max_header_list_size / max_frame_size) + 8, and it is infinity
when max_header_list_size is infinity. The derived value tracks the
byte bound, so a caller that raises max_header_list_size keeps the
CONTINUATION frames that carry the larger field section.
-type stream_state() ::
idle | reserved_local | reserved_remote | open | half_closed_local | half_closed_remote |
closed.
Functions
Return the connection send window in octets.
This is the credit the peer granted for DATA payloads on every stream of the
connection, less what send_data/4 spent (RFC 9113 Section 6.9.1). The
value is negative after a SETTINGS_INITIAL_WINDOW_SIZE shrink that overtakes
the credit already spent, and send_data/4 sends nothing until WINDOW_UPDATE
takes it positive again.
-spec encoder_table_entries(conn()) -> non_neg_integer().
Return the number of live entries in the dynamic table of the HPACK encoder.
The count moves with every field that send_headers/4 inserts and with
every eviction that the insert forces (RFC 7541 Section 4.4). A field whose
name is in hpack_index_policy never inserts, so a caller that tunes the
policy reads its effect here.
-spec encoder_table_size(conn()) -> non_neg_integer().
Return the size of the dynamic table of the HPACK encoder in octets.
The size counts every live entry as name, value and the 32 octets of overhead of RFC 7541 Section 4.1. It never exceeds the SETTINGS_HEADER_TABLE_SIZE that the peer sent.
Create a new HTTP/2 connection with default settings.
Create a new HTTP/2 connection with custom settings.
-spec open_stream(conn()) -> {ok, nhttp_lib:stream_id(), conn()} | {error, connection_closing | max_streams_reached | stream_ids_exhausted}.
Open a new stream and return its ID.
Stream identifiers grow by two per stream and end at 2^31-1. When the next
identifier is past that bound, the call answers {error, stream_ids_exhausted}
and leaves the connection unchanged. The caller must open a new connection
for further requests (RFC 9113 Section 5.1.1).
Return the settings the peer sent, merged over the defaults.
The map carries the last value of every SETTINGS parameter the peer sent,
and the default of RFC 9113 Section 6.5.2 for the rest. initial_window_size
is the send window that a new stream starts with, and max_frame_size bounds
the payload of one DATA frame.
Generate the connection preface for this role. Client sends: magic + SETTINGS. Server sends: SETTINGS.
-spec queued_streams(conn()) -> non_neg_integer().
Return the number of streams with an entry in the send queue.
-spec recv(conn(), binary()) -> recv_result().
Process incoming data and return events.
The four-tuple arm carries frames for the caller to write: SETTINGS ACK, PING ACK and RST_STREAM, then the DATA frames the send queue emitted.
-spec send_buffer_bytes(conn()) -> non_neg_integer().
Return the octets the send queue holds on the connection.
-spec send_buffer_bytes(conn(), nhttp_lib:stream_id()) -> non_neg_integer().
Return the octets the send queue holds for one stream, 0 without an entry.
-spec send_data(conn(), nhttp_lib:stream_id(), iodata(), fin()) -> send_result().
Send every DATA frame that the current credit allows.
The payload is split at the peer max_frame_size and bounded by the
connection send window and the stream send window. A payload that fits
in the credit comes back as {ok, Conn, Frames}. Frames is one DATA
frame or a list of DATA frames, and END_STREAM sits on the last frame
only. A payload that outruns the credit comes back as
{partial, Conn, Frames, Rest, EndStream, Window}. Frames carries every
frame the credit paid for and none of them carries END_STREAM. Rest is
the remainder the caller must offer again after WINDOW_UPDATE, and
Window is the credit that is left, zero or below. A {partial, ...}
return therefore means one thing: the credit ran out.
The frames share the octets of the payload. Rest is a copy when the
payload is more than four times its size, so a small remainder does not
retain a large payload.
An empty payload with fin is sent at any window value, because a frame
without payload consumes no flow-control credit (RFC 9113 Section 6.9.1).
An empty payload with nofin at a window of zero or below is held back as
{partial, ...}. The frame is legal, but it carries nothing, so the codec
declines to spend a frame on it.
With send_queue => true the call never answers {partial, ...}. A
payload that outruns the credit comes back as
{queued, Conn, Frames, Buffered}, where Buffered is the octet count
of this stream that the codec holds. An empty payload with nofin
answers {ok, Conn, []} at any window, an empty fin behind pending
octets rides the last drained frame, and a stream whose pending entry
carries fin answers {error, {stream_closed, StreamId}}.
-spec send_goaway(conn(), error_code(), binary()) -> send_result().
Send GOAWAY to initiate graceful shutdown.
-spec send_headers(conn(), nhttp_lib:stream_id(), nhttp_lib:headers(), fin()) -> {ok, conn(), iodata()} | {error, send_error()}.
Send HEADERS frame for a new request/response or trailers.
A stream with octets in the send queue answers
{error, {data_pending, StreamId}}.
-spec send_ping(conn(), <<_:64>>) -> send_result().
Send PING frame with caller-supplied 8-byte opaque data. The caller is
responsible for generating the opaque value (e.g. via
crypto:strong_rand_bytes(8)) and matching it against the PING_ACK event.
-spec send_rst_stream(conn(), nhttp_lib:stream_id(), error_code()) -> send_result().
Send RST_STREAM to cancel a stream.
The call removes the stream and purges its send queue entry. The purge changes no window: octets already emitted stay debited until the peer credits stream 0 (RFC 9113 Section 6.9.1).
-spec send_window_update(conn(), nhttp_lib:stream_id() | connection, pos_integer()) -> send_result().
Send WINDOW_UPDATE for the connection or for a stream.
The increment is added to the local receive window. An increment that takes
that window past 2^31-1 is refused as {recv_window_overflow, Target} and
nothing is sent (RFC 9113 Section 6.9.1). A stream that the codec no longer
tracks answers {ok, Conn, []}: the stream closed, no credit is owed, and
the peer ignores a frame on a closed stream (RFC 9113 Section 5.1).
-spec set_peer(conn(), nhttp_lib:peer()) -> conn().
Record the peer address on the connection. Called once after the socket is
accepted (or connected) so server-built nhttp_lib:request/0 maps and
client-built nhttp_lib:response/0 events carry the correct remote peer.
-spec stream_send_window(conn(), nhttp_lib:stream_id()) -> {ok, integer()} | {error, {unknown_stream, nhttp_lib:stream_id()}}.
Return the send window of one stream in octets.
The value is the credit the peer granted for DATA payloads on that stream,
less what send_data/4 spent (RFC 9113 Section 6.9.1). A stream that
open_stream/1 created and send_headers/4 has not yet moved out of idle
carries a window too, and a SETTINGS_INITIAL_WINDOW_SIZE change reaches it.
A stream the codec does not track answers {error, {unknown_stream, Id}}.
-spec stream_stats(conn()) -> #{active := non_neg_integer(), peer_opened := non_neg_integer(), peer_reset := non_neg_integer()}.
Return the stream counters for this connection.
active is the number of streams in the "open" state or in either
"half-closed" state, the set that SETTINGS_MAX_CONCURRENT_STREAMS bounds
(RFC 9113 Section 5.1.2). peer_opened is the total number of streams that
the peer opened. peer_reset is the number of streams that the peer
terminated with RST_STREAM.
SETTINGS_MAX_CONCURRENT_STREAMS bounds the streams that are open at one
instant. It does not bound stream turnover. A peer that alternates HEADERS
and RST_STREAM holds active at a low value and drives peer_opened and
peer_reset without limit. RFC 9113 Section 10.5 tells an implementation to
track such use and to set a limit on it.
This library holds no clock, so it counts events only. The caller reads these
counters to apply a rate per unit of time. To let the connection refuse the
peer on its own, set max_reset_streams in the local settings. The
connection then fails with a connection error of type ENHANCE_YOUR_CALM when
peer_reset is more than max_reset_streams + (peer_opened div 2). The
default is infinity, which counts without a limit.