nhttp (nhttp v1.4.0)
View SourceServer public API for nhttp.
This module provides convenience functions for starting HTTP servers.
For production use, embed nhttp_listener in your supervision tree.
Quick Start
{ok, Pid} = nhttp:start_link(#{
port => 8080,
handler => my_handler
}),
nhttp:stop(Pid).Production Usage
For production, add the listener to your supervision tree:
ChildSpec = nhttp_listener:child_spec(my_http_listener, #{
port => 8080,
handler => my_handler
}),
{ok, {SupFlags, [ChildSpec | OtherChildren]}}.TLS and SNI
A single TCP/TLS listener can serve several virtual hosts by attaching
SNI options to the tls map (see tls/0). Both shapes are passed
through to the underlying ssl server options unchanged.
Static lookup with sni_hosts:
{ok, _} = nhttp:start_link(my_server, #{
port => 8443,
handler => my_handler,
versions => [http1_1, http2],
tls => #{
certfile => "default.pem",
keyfile => "default.key",
sni_hosts => [
{"a.example.com",
[{certfile, "vhost_a.pem"}, {keyfile, "vhost_a.key"}]},
{"b.example.com",
[{certfile, "vhost_b.pem"}, {keyfile, "vhost_b.key"}]}
]
}
}).Dynamic resolution with sni_fun (return undefined to fall back to
the listener's default certfile/keyfile):
SniFun = fun(ServerName) ->
case my_cert_store:lookup(ServerName) of
{ok, CertOpts} -> CertOpts;
not_found -> undefined
end
end,
{ok, _} = nhttp:start_link(my_server, #{
port => 8443,
handler => my_handler,
versions => [http1_1, http2],
tls => #{
certfile => "default.pem",
keyfile => "default.key",
sni_fun => SniFun
}
}).sni_hosts and sni_fun may be combined and are TCP/TLS only. HTTP/3
listeners ignore them.
HTTP/2 in cleartext (h2c)
versions => [http2] without tls serves HTTP/2 over plain TCP with
prior knowledge (RFC 9113 §3.3). The client sends the HTTP/2 connection
preface first, as curl --http2-prior-knowledge and gRPC clients do.
Upgrade: h2c is not supported. Requests carry the :scheme that the
client sends.
{ok, Pid} = nhttp:start_link(my_srv, #{
port => 8080,
handler => my_handler,
versions => [http2]
}).versions => [http1_1, http2] without tls serves both on one port. Each
connection gets HTTP/2 when its first 14 bytes are PRI * HTTP/2.0, the
start of the connection preface (RFC 9113 §3.4), and HTTP/1.1 from the
first byte that differs. A connection that sends no deciding bytes
before the request timeout, or the idle timeout when it is shorter,
is closed without a response.
Use cleartext HTTP/2 only behind a TLS terminator or inside a trusted
network. http3 always needs tls.
Mixed h1/h2/h3 listeners
versions => [http1_1, http2, http3] serves all three from one call. h1
and h2 share a single TCP/TLS listener (ALPN-negotiated). h3 runs over a
separate UDP/QUIC listener bound, by convention, on the same port number.
{ok, Pid} = nhttp:start_link(my_srv, #{
port => 443,
handler => my_handler,
versions => [http1_1, http2, http3],
tls => #{certfile => "cert.pem", keyfile => "key.pem"}
}),
{ok, TcpPort} = nhttp:get_port(Pid), % h1/h2 (TCP)
{ok, QuicPort} = nhttp:get_port(Pid, quic). % h3 (UDP)max_connections applies per transport: a mixed listener admits up
to max_connections TCP connections and up to max_connections QUIC
connections, each with its own counter. The two never share a budget.
With port => 0 the TCP and UDP transports bind different ephemeral
ports. get_port/1 returns the TCP port. Read the UDP port with
get_port(Pid, quic) or get_ports/1. Never assume the two are equal.
Alt-Svc advertisement
A mixed listener auto-emits Alt-Svc: h3=":<quic_port>"; ma=86400 on
every HTTP/1.1 and HTTP/2 response so clients discover and upgrade to h3
(RFC 7838 §3, RFC 9114 §3.1). With port => 0 the advertised port is the
QUIC ephemeral port. Tune the freshness lifetime with
alt_svc => #{ma => Seconds}, or disable advertisement with
alt_svc => false. A handler overrides the header by setting its own
alt-svc on the response, and suppresses it by setting alt-svc to an
empty value.
Advertisement tracks live QUIC readiness, not static config (RFC 7838
§2.1). h3 is advertised only while the QUIC transport is accepting. Once
it drains or goes down the response carries Alt-Svc: clear instead, so
clients drop the cached alternative rather than waste a UDP attempt and
fall back (RFC 7838 §4). Draining a mixed listener stops advertising
before it stops accepting.
Response compression
A reply body of a type in compress_mime_types and at least
compression_threshold bytes is gzip or deflate compressed when
Accept-Encoding allows it. Each such response gets
vary: accept-encoding, merged with any vary from the handler.
The body is sent unchanged when the handler set content-encoding, when
cache-control holds no-transform, or when the status is 1xx, 204, 206
or 304. Streamed bodies are never compressed. HEAD gets the same headers
as GET.
0-RTT
QUIC 0-RTT (early data) is not enabled: nquic refuses early data unless a replay-protection module is configured, which this server never sets. No application bytes are processed before the handshake completes, so a replayed non-idempotent method cannot reach the handler (RFC 8470, RFC 9001 §9.2).
Summary
Types
An erlang:spawn_opt/4 option for the processes that run handler code: the
connection processes of HTTP/1.1, HTTP/2 and HTTP/3, and the stream workers.
link and monitor are not allowed, because nhttp owns them. Default [].
Where handle_request/2 runs on HTTP/2. inline: in the connection
process, in stream order, as on HTTP/1.1 and HTTP/3. A request with a
body runs in a worker. worker: in one worker per stream. Default
inline.
Delay before the response headers of a {reply, _, _} handler result on
HTTP/2, in milliseconds. {uniform, MinMs, MaxMs} draws a value in that
range for each response. Error responses, producer streams and WebSocket
upgrades are not delayed. Default 0.
Credit policy for an HTTP/2 receive window. eager sends a WINDOWUPDATE
for each body chunk as soon as the handler consumed it. {threshold, N}
holds the credit until the uncredited octets reach N, then sends the
accumulated total. {delay, Ms} sends the credit for each chunk Ms
milliseconds after the handler consumed it. on_response sends the
credit for the body of a request ahead of the response headers of its
`{reply, , _}result. On the connection windowh2_credit_batchholds that credit until the responded octets reach the batch, then sends one WINDOW_UPDATE of exactly the batch.neversends no credit. Defaulteager`.
Settings of the nquic listener that serves http3. nhttp passes them to
nquic:listen/2, which checks the values.
Connection timeouts (milliseconds, or infinity).
Functions
Best-effort count of in-flight connections across every nhttp listener in this VM.
Count of in-flight requests across every nhttp listener in this VM.
Backed by nhttp_otel's active-requests metric (always on for queries
even when otel export is disabled).
Best-effort count of live WebSocket sessions across every nhttp listener
in this VM.
Incremented after the upgrade response is on the wire, decremented when
the session terminates. Same brutal-kill caveat as active_connections/0.
Generate a child spec for embedding the listener in your supervisor with an auto-generated id. Use this form when you do not need to look the listener up by name later.
Generate a child spec for embedding the listener in your supervisor
under a caller-supplied name.
The supervisor child id is {nhttp_listener, Name} and the listener
registers itself under Name when it starts.
Drain connections with the default 10-second timeout.
Stops the acceptor sub-supervisor, signals every live connection to
wind down, and blocks until they exit or the timeout elapses. The
listener itself stays up. Call stop/1 afterwards to tear it down.
Drain connections with a custom timeout (milliseconds or infinity).
See drain/1 for the behaviour. The listener itself stays up.
Get the TCP port a server is listening on.
Returns the TCP/TLS transport's port. For a QUIC-only server it returns
the sole transport's UDP port. Useful when port 0 was specified to get an
ephemeral port.
A mixed [http1_1, http2, http3] server binds two transports: TCP/TLS
for h1/h2 and UDP/QUIC for h3. With port => 0 they bind different
ephemeral ports. Use get_port/2 or get_ports/1 to read the UDP port.
Get the bound port for a specific transport (tcp or quic).
tcp covers the h1/h2 listener, quic the h3 listener. Returns
{error, {server, {no_transport, Kind}}} when the server does not serve
that transport.
Get every bound transport port as a map keyed by transport.
A single-transport server yields a one-entry map. A mixed server yields
#{tcp => P1, quic => P2}. With port => 0 the two ports differ.
Look up a request header by name (case-insensitive).
Top-level convenience wrapper over nhttp_req:header/2. Returns
undefined when the header is missing.
Look up a request header by name (case-insensitive), with a default.
Top-level convenience wrapper over nhttp_req:header/3.
Start an unnamed HTTP server linked to the calling process.
Returns the supervisor pid. For production, use
nhttp_listener:child_spec/1,2 to embed in your supervision tree
instead.
OpenTelemetry can be enabled via #{otel => true} or
#{otel => #{traces => true, metrics => true}}.
Start a named HTTP server linked to the calling process.
Name follows the standard gen_server registration shapes
(atom(), {local, atom()}, {global, term()}, {via, module(), term()}) and is also threaded into log / otel attributes as
listener_name.
Stop a running server immediately.
Stop a running server with options. Options
Types
-type authority() :: nhttp_lib:authority().
-type conn_spawn_opt() :: {priority, erlang:priority_level()} | {fullsweep_after, non_neg_integer()} | {min_heap_size, non_neg_integer()} | {min_bin_vheap_size, non_neg_integer()} | {max_heap_size, erlang:max_heap_size()} | {message_queue_data, erlang:message_queue_data()} | {async_dist, boolean()}.
An erlang:spawn_opt/4 option for the processes that run handler code: the
connection processes of HTTP/1.1, HTTP/2 and HTTP/3, and the stream workers.
link and monitor are not allowed, because nhttp owns them. Default [].
Under load, old-generation garbage can hold most of the heap of a
connection. {fullsweep_after, 0} collects it on every garbage collection.
Each collection then shrinks the heap to the live data, and HTTP throughput
drops. A min_heap_size that holds the allocations of one request restores
the throughput, but every active connection then holds at least that heap.
-type h2_dispatch() :: worker | inline.
Where handle_request/2 runs on HTTP/2. inline: in the connection
process, in stream order, as on HTTP/1.1 and HTTP/3. A request with a
body runs in a worker. worker: in one worker per stream. Default
inline.
-type h2_response_delay() :: non_neg_integer() | {uniform, non_neg_integer(), non_neg_integer()}.
Delay before the response headers of a {reply, _, _} handler result on
HTTP/2, in milliseconds. {uniform, MinMs, MaxMs} draws a value in that
range for each response. Error responses, producer streams and WebSocket
upgrades are not delayed. Default 0.
-type h2_window_policy() :: eager | {threshold, pos_integer()} | {delay, non_neg_integer()} | on_response | never.
Credit policy for an HTTP/2 receive window. eager sends a WINDOWUPDATE
for each body chunk as soon as the handler consumed it. {threshold, N}
holds the credit until the uncredited octets reach N, then sends the
accumulated total. {delay, Ms} sends the credit for each chunk Ms
milliseconds after the handler consumed it. on_response sends the
credit for the body of a request ahead of the response headers of its
`{reply, , _}result. On the connection windowh2_credit_batchholds that credit until the responded octets reach the batch, then sends one WINDOW_UPDATE of exactly the batch.neversends no credit. Defaulteager`.
-type header_name() :: nhttp_lib:header_name().
-type header_value() :: nhttp_lib:header_value().
-type headers() :: nhttp_lib:headers().
-type method() :: nhttp_lib:method().
-type name() :: nhttp_listener:name().
-type opts() :: #{acceptor_count => pos_integer(), alt_svc => #{ma => non_neg_integer()} | false, backlog => pos_integer(), buffer => pos_integer(), h2_connection_window_policy => h2_window_policy(), h2_credit_batch => non_neg_integer(), h2_dispatch => h2_dispatch(), h2_initial_window_size => 1..2147483647, h2_max_frame_size => 16384..16777215, h2_response_delay => h2_response_delay(), h2_settings => nhttp_h2:settings(), h2_stream_window_policy => h2_window_policy(), handler := module(), handler_args => term(), compression => boolean(), compression_level => 1..9, compression_threshold => non_neg_integer(), conn_spawn_opts => [conn_spawn_opt()], max_connections => pos_integer() | infinity, max_header_name_length => pos_integer(), max_header_value_length => pos_integer(), max_header_size => pos_integer(), max_headers => pos_integer(), max_uri_length => pos_integer(), max_body_size => pos_integer(), nodelay => boolean(), port := inet:port_number(), versions => [http1_1 | http2 | http3], timeouts => timeouts(), tls => tls(), proxy_protocol => boolean() | proxy_protocol_opts(), quic => quic_opts()}.
-type peer() :: nhttp_lib:peer().
-type proxy_protocol_opts() :: #{version => v1 | v2 | both, timeout => timeout()}.
-type quic_error() :: term().
-type quic_opts() :: #{receivers => pos_integer(), ip => inet:ip_address(), ipv6_v6only => boolean(), transport => nquic:transport_opts(), cc => nquic:cc_opts()}.
Settings of the nquic listener that serves http3. nhttp passes them to
nquic:listen/2, which checks the values.
receivers: the number of UDP sockets on the QUIC port, bound withSO_REUSEPORT. Each socket has one receiver process, and the kernel spreads the peers over the sockets. Default: the number of online schedulers.ip: the local address of the QUIC sockets. Default{0,0,0,0}. An IPv6 address opens IPv6 sockets.{0,0,0,0,0,0,0,0}withipv6_v6only => falsealso accepts IPv4 peers. A host where IPv6 is off cannot bind an IPv6 address.ipv6_v6only: theIPV6_V6ONLYsocket option for an IPv6ip.transport:gso,pacingand the other send settings ofnquic:transport_opts/0.cc: the congestion controller,nquic:cc_opts/0.
-type request() :: nhttp_lib:request().
-type response() :: nhttp_lib:response().
-type scheme() :: nhttp_lib:scheme().
-type start_error() :: {invalid_opts, {nhttp_error:category(), nhttp_error:reason()}} | {listen_failed, inet:posix() | quic_error()} | {already_started, pid()}.
-type status() :: nhttp_lib:status().
-type stop_opts() :: #{drain => boolean(), drain_timeout => timeout(), force_kill_delay => non_neg_integer()}.
-type stream_id() :: nhttp_lib:stream_id().
-type timeouts() :: #{idle => timeout(), request => timeout(), body => timeout(), body_deadline => timeout(), send => timeout(), hibernate => timeout()}.
Connection timeouts (milliseconds, or infinity).
idle: keep-alive gap between requests; re-arms per packet. Default 60000.request: absolute deadline from the first byte of a request head to a complete head (HTTP/1.1). Caps slowloris drip regardless ofidle. On a cleartext listener withhttp1_1andhttp2, also the deadline from the accept to the bytes that choose the protocol. Default 30000.body: per-chunk idle while receiving a request body; re-arms per chunk. Default 60000.body_deadline: absolute deadline from head-complete to a fully received body (HTTP/1.1). Defaultinfinity(opt-in) so legitimate large or slow uploads are not aborted.send: socket send timeout. Default 30000.hibernate: quiet time after which a connection with nothing in flight hibernates. The wait consumes part of the idle budget, so the idle timeout still fires on time.0hibernates on every quiet moment,infinitynever. Default 1000.
-type tls() :: #{certfile := file:filename(), keyfile := file:filename(), cacertfile => file:filename(), verify => verify_none | verify_peer, sni_fun => fun((string()) -> [ssl:tls_server_option()] | undefined), sni_hosts => [{string(), [ssl:tls_server_option()]}], extra => [ssl:tls_server_option()]}.
-type version() :: nhttp_lib:version().
Functions
-spec active_connections() -> non_neg_integer().
Best-effort count of in-flight connections across every nhttp listener in this VM.
Backed by an atomics counter incremented on connection accept and
decremented on connection terminate. A brutal kill that bypasses
terminate/2 leaks the slot. The counter never goes below zero
because the reader floors at 0.
-spec active_requests() -> non_neg_integer().
Count of in-flight requests across every nhttp listener in this VM.
Backed by nhttp_otel's active-requests metric (always on for queries
even when otel export is disabled).
-spec active_ws_sessions() -> non_neg_integer().
Best-effort count of live WebSocket sessions across every nhttp listener
in this VM.
Incremented after the upgrade response is on the wire, decremented when
the session terminates. Same brutal-kill caveat as active_connections/0.
-spec child_spec(opts()) -> supervisor:child_spec().
Generate a child spec for embedding the listener in your supervisor with an auto-generated id. Use this form when you do not need to look the listener up by name later.
-spec child_spec(name(), opts()) -> supervisor:child_spec().
Generate a child spec for embedding the listener in your supervisor
under a caller-supplied name.
The supervisor child id is {nhttp_listener, Name} and the listener
registers itself under Name when it starts.
-spec drain(pid()) -> ok.
Drain connections with the default 10-second timeout.
Stops the acceptor sub-supervisor, signals every live connection to
wind down, and blocks until they exit or the timeout elapses. The
listener itself stays up. Call stop/1 afterwards to tear it down.
Drain connections with a custom timeout (milliseconds or infinity).
See drain/1 for the behaviour. The listener itself stays up.
-spec get_port(pid()) -> {ok, inet:port_number()} | {error, term()}.
Get the TCP port a server is listening on.
Returns the TCP/TLS transport's port. For a QUIC-only server it returns
the sole transport's UDP port. Useful when port 0 was specified to get an
ephemeral port.
A mixed [http1_1, http2, http3] server binds two transports: TCP/TLS
for h1/h2 and UDP/QUIC for h3. With port => 0 they bind different
ephemeral ports. Use get_port/2 or get_ports/1 to read the UDP port.
-spec get_port(pid(), tcp | quic) -> {ok, inet:port_number()} | {error, term()}.
Get the bound port for a specific transport (tcp or quic).
tcp covers the h1/h2 listener, quic the h3 listener. Returns
{error, {server, {no_transport, Kind}}} when the server does not serve
that transport.
-spec get_ports(pid()) -> #{tcp | quic => inet:port_number()}.
Get every bound transport port as a map keyed by transport.
A single-transport server yields a one-entry map. A mixed server yields
#{tcp => P1, quic => P2}. With port => 0 the two ports differ.
-spec header(header_name(), request()) -> header_value() | undefined.
Look up a request header by name (case-insensitive).
Top-level convenience wrapper over nhttp_req:header/2. Returns
undefined when the header is missing.
-spec header(header_name(), request(), Default) -> header_value() | Default.
Look up a request header by name (case-insensitive), with a default.
Top-level convenience wrapper over nhttp_req:header/3.
-spec start_link(opts()) -> {ok, pid()} | ignore | {error, start_error()}.
Start an unnamed HTTP server linked to the calling process.
Returns the supervisor pid. For production, use
nhttp_listener:child_spec/1,2 to embed in your supervision tree
instead.
OpenTelemetry can be enabled via #{otel => true} or
#{otel => #{traces => true, metrics => true}}.
-spec start_link(name(), opts()) -> {ok, pid()} | ignore | {error, start_error()}.
Start a named HTTP server linked to the calling process.
Name follows the standard gen_server registration shapes
(atom(), {local, atom()}, {global, term()}, {via, module(), term()}) and is also threaded into log / otel attributes as
listener_name.
-spec stop(pid()) -> ok.
Stop a running server immediately.
Stop a running server with options. Options:
drain => false(default): Stop immediately.drain_timeoutis rejected in this mode to surface configuration mistakes.drain => true: Drain connections, then stop. Defaults to a 10 seconddrain_timeoutif none is supplied.drain_timeout => timeout(): Override the drain deadline (ms orinfinity). Only meaningful whendrain => true. During drain:
- Stop accepting new connections
- Signal existing connections to complete and close
- Wait for connections to drain (up to
drain_timeout) - Force-close remaining connections after the timeout elapses