nquic_lib (nquic v1.0.0)
View SourceLibrary-mode API for nquic.
Library mode exposes the QUIC protocol as pure functions over an
opaque nquic:ctx() value. The owning process drives the socket,
receives packets, and flushes outgoing data. Compared to
gen_statem mode this avoids every gen_statem round trip on the
hot path.
Production use requires the caller to understand the protocol carefully; the compiler will not stop you from calling the library functions out of order.
Typical usage
{ok, Ctx} = nquic:accept(Listener),
{ok, Ctx1} = nquic_lib:takeover(Ctx),
{ok, Ctx2} = nquic_lib:upgrade_to_connected(Ctx1),
loop(Ctx2).
loop(Ctx) ->
case nquic_lib:recv_and_process(Ctx, 5000) of
{ok, Events, Ctx1} -> handle(Events), loop(Ctx1);
{error, _Reason, _} -> ok
end.Listener drain
A server connection's owner process is sent
{quic_drain, Listener} when its listener is torn down via
nquic:stop_listener/1,2 with mode => cascade (the default).
The owner SHOULD handle this message by closing the connection
(shutdown/1 for a graceful CONNECTION_CLOSE) and terminating.
An owner that ignores it is not leaked indefinitely (the connection
still closes on its idle timeout), but a prompt cascade depends on
owners honouring the signal. Client connections own their socket,
have no listener, and never receive it.
Summary
Functions
Buffer protocol events into the context.
Close the connection gracefully (transport scope, error code 0, empty reason).
Close the connection with the given options.
scope => transport (default) emits CONNECTION_CLOSE type 0x1c;
application emits type 0x1d (RFC 9000 §19.19).
Close a stream.
Flush pending frames into packets and send them. Encrypts queued frames, sends the resulting packets via the socket, and schedules any timer updates.
Flush pending frames without scheduling timers.
Use this when the caller will immediately call a recv function that
handles timers; avoids redundant cancel_timer / send_after
syscalls.
Process an incoming packet. Decrypts, decodes, and handles every frame in the packet. Returns the protocol events and the context with timers scheduled.
Like handle_packet/3 but carries the inbound ECN codepoint.
Process a GRO-coalesced datagram delivered as a single
{packet_batch, Source, Buf, GsoSize, ECN} message.
Splits the buffer per GsoSize bytes and runs handle_packet/4 on each
segment, consolidating events. Schedules timers once at the end.
Like handle_packet_batch/5 but without scheduling timers.
For callers that drain many datagrams per wakeup and call
schedule_timers/1 once at the end before flushing (the batch sibling of
handle_packet_notimers/3). Per-segment decode errors are dropped rather
than fatal, mirroring drain_packet_batch/6: an undecryptable packet in a
GRO-coalesced buffer must not tear down the connection (RFC 9000 §12.2).
Process an incoming packet without scheduling timers.
Use for batch processing: call this once per packet, then call
schedule_timers/1 once at the end before flushing.
Like handle_packet_notimers/3 but carries an ECN mark.
Handle a {quic_timeout, Type} expiration during the server handshake.
Phase is the encryption level the owner is currently driving
(initial until it observes {state_transition, handshake}, then
handshake). On a PTO this sends the probe at that level; the
established timeout/2 (which probes 1-RTT) is wrong before the
connection is established. Switch to timeout/2 once connected is
observed.
Initiate a client-side key update (RFC 9001 Section 6).
Rotates the 1-RTT traffic secrets and flips the key phase so the next
sent 1-RTT packet carries the new keys. No frame is emitted and no
packet is sent here; the owner's next flush/1 (or any send) carries
the rotation, and the peer's reply confirms it. Returns
{error, key_update_pending} if a previously initiated update has not
yet been confirmed by the peer.
Point-in-time stream writability probe on a library-mode context.
Pure projection on the conn_state held inside the nquic:ctx():
returns true when the stream exists, its send side is not
terminal, and one byte fits under current connection-flow,
stream-flow, and congestion limits. false otherwise (including
unknown streams). A false does not guarantee the next send
succeeds, and a true can be stale before the caller acts; it is a
between-recv-turns probe, not a poll loop.
Open a new stream.
Project path-level statistics from a library-mode context.
Pure projection on the conn_state held inside the nquic:ctx(). No
message hops, no syscalls. See nquic_conn:path_stats/1 for the field
list.
Read available data from a stream buffer.
Does not block. Returns {ok, Data, IsFin, Ctx} where IsFin marks
end of stream.
Receive and process the next packet or timeout.
Waits for either a {packet, Source, Bin} message or a
{quic_timeout, Type} timer expiration. Equivalent to
recv_and_process(Ctx, infinity).
Receive and process the next packet or timeout, with timeout.
Returns {ok, [], Ctx} if no message arrives within Timeout ms.
Receive and process ALL available packets, then schedule timers and
flush once.
For ctxs upgraded with upgrade_to_connected/1, drains both the
mailbox and the kernel queue (via non-blocking socket:recvfrom/3).
For dispatched-mode ctxs (sharing the listener socket), drains only
the mailbox; polling the shared socket would steal packets routed
to sibling connections. Blocks up to Timeout ms for the first
packet if none are available.
The batch equivalent of recv_direct/1 / recv_and_process/1.
Amortises timer scheduling and flushing across many packets.
Like recv_batch/1 with a timeout on the first packet.
Receive a buffered DATAGRAM.
Returns the oldest datagram from the buffer, or {error, empty} if
none are available. Use buffer_events/2 after handle_packet/3 to
move received datagrams into the buffer.
Receive and process the next packet directly from the socket.
For connections that called upgrade_to_connected/1, reads packets
directly via socket:recvfrom/3 instead of waiting for {packet,...}
messages. Also services timer expirations.
Like recv_direct/1 with a timeout.
Process every buffered {packet, Source, Bin} message in the current
process mailbox.
Call after takeover/1 and/or upgrade_to_connected/1 to handle
packets that arrived during the ownership transition window. Returns
accumulated events, or {error, Reason, Ctx} on transport error.
Reset a stream with an error code.
Compute and schedule timer actions for the current protocol state.
Call this after a batch of handle_packet_notimers/3 calls and before
flush/1. Computes loss detection, idle, and ACK delay timers and
schedules them as erlang:send_after messages.
Send data on a stream. Queues the data, flushes pending frames into packets, and writes them to the socket.
Send an unreliable DATAGRAM frame.
Send data with FIN on a stream.
Queue data + FIN on a stream without flushing.
Use to batch multiple stream sends into a single subsequent flush/1
call.
Seed a server-side nquic:ctx/0 for an owner that drives the
handshake itself, from the first Initial packet's options.
Opts is the option map the receiver builds for a new connection
(role, socket, peer, dcid/odcid, version, dispatchtable, listener,
certs, alpn, static_key, transport params, ...). Builds the same
#conn_state{} as nquic_conn_statem:init/1 (via
nquic_conn_init:new_conn_state/1), registers SCID -> self() in the
dispatch table so the connection's own CIDs route to this owner, and
returns a context in the initial phase.
The owner then drives initial -> handshake -> established by calling
handle_packet/3,4 + flush/1 on inbound `{packet, }messages andhandshaketimeout/3on{quic_timeout, }, untilhandle_packetemitsconnected`. There is no export, accept queue, or takeover: the
owner is the registrant from the first packet, so the connection's CID
never resolves to a non-owner.
Shut down a library-mode connection.
Sends CONNECTION_CLOSE with error code 0, flushes pending packets,
cancels all timers, and explicitly closes the connected UDP socket
(if any). Idempotent. Always returns ok.
The caller is expected to clean up dispatch table entries and exit
the process itself after this returns.
Like shutdown/1 but sends an application error code.
Take ownership of a library-mode context in this process.
Re-registers all local connection IDs in the dispatch table to point to
self(). Call this when a process receives a nquic:ctx() from another
process (e.g. an accept loop handing off to a per-connection handler).
After takeover/1, call recv_pending/1 to process any packets that
were buffered in the previous owner's mailbox during the transition.
Handle a QUIC timer expiration.
Called when a {quic_timeout, Type} message is received by the owner.
Processes the timeout (loss detection, idle, path validation),
flushes any resulting packets, and returns protocol events.
Upgrade the context to use a connected UDP socket.
Opens a new UDP socket on the same port as the listener, then
connect(2)s it to the peer. The kernel routes packets from this peer
directly to the new socket, bypassing the receiver dispatch entirely.
After this call, use recv_direct/1,2 instead of
recv_and_process/1,2.
Functions
-spec buffer_events([nquic_protocol:event()], nquic:ctx()) -> {[nquic_protocol:event()], nquic:ctx()}.
Buffer protocol events into the context.
Moves {datagram_received, Data} events into the datagram buffer
(bounded, drops oldest on overflow). Returns remaining non-datagram
events and the updated context.
Close the connection gracefully (transport scope, error code 0, empty reason).
-spec close(nquic:ctx(), nquic:close_opts()) -> {ok, nquic:ctx()}.
Close the connection with the given options.
scope => transport (default) emits CONNECTION_CLOSE type 0x1c;
application emits type 0x1d (RFC 9000 §19.19).
-spec close_stream(nquic:ctx(), nquic:stream_id()) -> {ok, nquic:ctx()} | {error, nquic_error:any_reason()}.
Close a stream.
Flush pending frames into packets and send them. Encrypts queued frames, sends the resulting packets via the socket, and schedules any timer updates.
Flush pending frames without scheduling timers.
Use this when the caller will immediately call a recv function that
handles timers; avoids redundant cancel_timer / send_after
syscalls.
-spec handle_packet(nquic:ctx(), nquic_socket:sockaddr(), binary()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Process an incoming packet. Decrypts, decodes, and handles every frame in the packet. Returns the protocol events and the context with timers scheduled.
-spec handle_packet(nquic:ctx(), nquic_socket:sockaddr(), binary(), nquic_socket:ecn_mark()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Like handle_packet/3 but carries the inbound ECN codepoint.
-spec handle_packet_batch(nquic:ctx(), nquic_socket:sockaddr(), binary(), pos_integer(), nquic_socket:ecn_mark()) -> {ok, [nquic_protocol:event()], nquic:ctx()}.
Process a GRO-coalesced datagram delivered as a single
{packet_batch, Source, Buf, GsoSize, ECN} message.
Splits the buffer per GsoSize bytes and runs handle_packet/4 on each
segment, consolidating events. Schedules timers once at the end.
-spec handle_packet_batch_notimers(nquic:ctx(), nquic_socket:sockaddr(), binary(), pos_integer(), nquic_socket:ecn_mark()) -> {ok, [nquic_protocol:event()], nquic:ctx()}.
Like handle_packet_batch/5 but without scheduling timers.
For callers that drain many datagrams per wakeup and call
schedule_timers/1 once at the end before flushing (the batch sibling of
handle_packet_notimers/3). Per-segment decode errors are dropped rather
than fatal, mirroring drain_packet_batch/6: an undecryptable packet in a
GRO-coalesced buffer must not tear down the connection (RFC 9000 §12.2).
-spec handle_packet_notimers(nquic:ctx(), nquic_socket:sockaddr(), binary()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Process an incoming packet without scheduling timers.
Use for batch processing: call this once per packet, then call
schedule_timers/1 once at the end before flushing.
-spec handle_packet_notimers(nquic:ctx(), nquic_socket:sockaddr(), binary(), nquic_socket:ecn_mark()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Like handle_packet_notimers/3 but carries an ECN mark.
-spec handshake_timeout(nquic:ctx(), initial | handshake, nquic_protocol:timer_type()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Handle a {quic_timeout, Type} expiration during the server handshake.
Phase is the encryption level the owner is currently driving
(initial until it observes {state_transition, handshake}, then
handshake). On a PTO this sends the probe at that level; the
established timeout/2 (which probes 1-RTT) is wrong before the
connection is established. Switch to timeout/2 once connected is
observed.
Initiate a client-side key update (RFC 9001 Section 6).
Rotates the 1-RTT traffic secrets and flips the key phase so the next
sent 1-RTT packet carries the new keys. No frame is emitted and no
packet is sent here; the owner's next flush/1 (or any send) carries
the rotation, and the peer's reply confirms it. Returns
{error, key_update_pending} if a previously initiated update has not
yet been confirmed by the peer.
-spec is_writable(nquic:ctx(), nquic:stream_id()) -> boolean().
Point-in-time stream writability probe on a library-mode context.
Pure projection on the conn_state held inside the nquic:ctx():
returns true when the stream exists, its send side is not
terminal, and one byte fits under current connection-flow,
stream-flow, and congestion limits. false otherwise (including
unknown streams). A false does not guarantee the next send
succeeds, and a true can be stale before the caller acts; it is a
between-recv-turns probe, not a poll loop.
-spec open_stream(nquic:ctx(), nquic:stream_opts()) -> {ok, nquic:stream_id(), nquic:ctx()} | {error, term()}.
Open a new stream.
-spec path_stats(nquic:ctx()) -> nquic_loss:path_stats().
Project path-level statistics from a library-mode context.
Pure projection on the conn_state held inside the nquic:ctx(). No
message hops, no syscalls. See nquic_conn:path_stats/1 for the field
list.
-spec recv(nquic:ctx(), nquic:stream_id()) -> {ok, binary(), boolean(), nquic:ctx()} | {error, term()}.
Read available data from a stream buffer.
Does not block. Returns {ok, Data, IsFin, Ctx} where IsFin marks
end of stream.
-spec recv_and_process(nquic:ctx()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Receive and process the next packet or timeout.
Waits for either a {packet, Source, Bin} message or a
{quic_timeout, Type} timer expiration. Equivalent to
recv_and_process(Ctx, infinity).
-spec recv_and_process(nquic:ctx(), timeout()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Receive and process the next packet or timeout, with timeout.
Returns {ok, [], Ctx} if no message arrives within Timeout ms.
-spec recv_batch(nquic:ctx()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Receive and process ALL available packets, then schedule timers and
flush once.
For ctxs upgraded with upgrade_to_connected/1, drains both the
mailbox and the kernel queue (via non-blocking socket:recvfrom/3).
For dispatched-mode ctxs (sharing the listener socket), drains only
the mailbox; polling the shared socket would steal packets routed
to sibling connections. Blocks up to Timeout ms for the first
packet if none are available.
The batch equivalent of recv_direct/1 / recv_and_process/1.
Amortises timer scheduling and flushing across many packets.
-spec recv_batch(nquic:ctx(), timeout()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Like recv_batch/1 with a timeout on the first packet.
Receive a buffered DATAGRAM.
Returns the oldest datagram from the buffer, or {error, empty} if
none are available. Use buffer_events/2 after handle_packet/3 to
move received datagrams into the buffer.
-spec recv_direct(nquic:ctx()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Receive and process the next packet directly from the socket.
For connections that called upgrade_to_connected/1, reads packets
directly via socket:recvfrom/3 instead of waiting for {packet,...}
messages. Also services timer expirations.
-spec recv_direct(nquic:ctx(), timeout()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Like recv_direct/1 with a timeout.
-spec recv_pending(nquic:ctx()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Process every buffered {packet, Source, Bin} message in the current
process mailbox.
Call after takeover/1 and/or upgrade_to_connected/1 to handle
packets that arrived during the ownership transition window. Returns
accumulated events, or {error, Reason, Ctx} on transport error.
-spec reset_stream(nquic:ctx(), nquic:stream_id(), non_neg_integer()) -> {ok, nquic:ctx()} | {error, term()}.
Reset a stream with an error code.
Compute and schedule timer actions for the current protocol state.
Call this after a batch of handle_packet_notimers/3 calls and before
flush/1. Computes loss detection, idle, and ACK delay timers and
schedules them as erlang:send_after messages.
-spec send(nquic:ctx(), nquic:stream_id(), iodata()) -> {ok, nquic:ctx()} | {error, term(), nquic:ctx()} | {error, term()}.
Send data on a stream. Queues the data, flushes pending frames into packets, and writes them to the socket.
-spec send_datagram(nquic:ctx(), binary()) -> {ok, nquic:ctx()} | {error, nquic_error:any_reason()}.
Send an unreliable DATAGRAM frame.
-spec send_fin(nquic:ctx(), nquic:stream_id(), iodata()) -> {ok, nquic:ctx()} | {error, term(), nquic:ctx()} | {error, term()}.
Send data with FIN on a stream.
-spec send_fin_noflush(nquic:ctx(), nquic:stream_id(), iodata()) -> {ok, nquic:ctx()} | {error, term(), nquic:ctx()} | {error, term()}.
Queue data + FIN on a stream without flushing.
Use to batch multiple stream sends into a single subsequent flush/1
call.
Seed a server-side nquic:ctx/0 for an owner that drives the
handshake itself, from the first Initial packet's options.
Opts is the option map the receiver builds for a new connection
(role, socket, peer, dcid/odcid, version, dispatchtable, listener,
certs, alpn, static_key, transport params, ...). Builds the same
#conn_state{} as nquic_conn_statem:init/1 (via
nquic_conn_init:new_conn_state/1), registers SCID -> self() in the
dispatch table so the connection's own CIDs route to this owner, and
returns a context in the initial phase.
The owner then drives initial -> handshake -> established by calling
handle_packet/3,4 + flush/1 on inbound `{packet, }messages andhandshaketimeout/3on{quic_timeout, }, untilhandle_packetemitsconnected`. There is no export, accept queue, or takeover: the
owner is the registrant from the first packet, so the connection's CID
never resolves to a non-owner.
-spec shutdown(nquic:ctx()) -> ok.
Shut down a library-mode connection.
Sends CONNECTION_CLOSE with error code 0, flushes pending packets,
cancels all timers, and explicitly closes the connected UDP socket
(if any). Idempotent. Always returns ok.
The caller is expected to clean up dispatch table entries and exit
the process itself after this returns.
-spec shutdown(nquic:ctx(), non_neg_integer(), binary()) -> ok.
Like shutdown/1 but sends an application error code.
Take ownership of a library-mode context in this process.
Re-registers all local connection IDs in the dispatch table to point to
self(). Call this when a process receives a nquic:ctx() from another
process (e.g. an accept loop handing off to a per-connection handler).
After takeover/1, call recv_pending/1 to process any packets that
were buffered in the previous owner's mailbox during the transition.
-spec timeout(nquic:ctx(), nquic_protocol:timer_type()) -> {ok, [nquic_protocol:event()], nquic:ctx()} | {error, term(), nquic:ctx()}.
Handle a QUIC timer expiration.
Called when a {quic_timeout, Type} message is received by the owner.
Processes the timeout (loss detection, idle, path validation),
flushes any resulting packets, and returns protocol events.
Upgrade the context to use a connected UDP socket.
Opens a new UDP socket on the same port as the listener, then
connect(2)s it to the peer. The kernel routes packets from this peer
directly to the new socket, bypassing the receiver dispatch entirely.
After this call, use recv_direct/1,2 instead of
recv_and_process/1,2.
Warning: handshake race under burst load
The connected socket must bind to the listener's port with
SO_REUSEPORT (Linux requires all sockets sharing a port to have it
if any does). Each upgraded connection adds another member to the
reuseport group. For a new client's Initial (no existing 4-tuple
match), Linux hashes across the group and may land on a connected
socket whose peer does not match; compute_score() returns -1 and
the kernel drops the packet rather than retrying another slot. The
new client then stalls until wait_established times out.
Under sequential-but-bursty connects the observed loss rate is ~1%
per new handshake once many connections are held open. The proper
fix is an SO_ATTACH_REUSEPORT_CBPF filter pinning fallback hashes
to the listener slot (exact 4-tuple matches bypass it and keep the
fast path). OTP 28's socket module does not yet expose this option
and setopt_native cannot marshal the required struct sock_fprog
pointer, so the fix is pending.
Callers that accept many concurrent connections should avoid this helper until the library-level fix lands, or tolerate the rare timeout at the accept layer.