Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[1.4.0] - 2026-10-28
Added
conn_spawn_opts => [nhttp:conn_spawn_opt()]: spawn options for the connection processes of HTTP/1.1, HTTP/2 and HTTP/3 and for the stream workers, for example{fullsweep_after, 0}or{min_heap_size, Words}. The listener rejectslink,monitorand a value thaterlang:spawn_opt/4does not accept. Default[]- Response compression adds
vary: accept-encodingto every response whose coding depends onAccept-Encoding. Avaryfield that the handler set is kept - HTTP/2 in cleartext with prior knowledge (h2c, RFC 9113 ยง3.3):
versions => [http2]withouttlsstarts. Onlyhttp3needstls - HTTP/1.1 and h2c on one cleartext port:
versions => [http1_1, http2]withouttlspicks the protocol of each connection from its first bytes, HTTP/2 when they are the connection preface. A connection that sends no deciding bytes before therequesttimeout is closed quic => nhttp:quic_opts(): thereceivers,ip,ipv6_v6only,transportandccsettings of the nquic listener that serves HTTP/3.ip => {0,0,0,0,0,0,0,0}withipv6_v6only => falseserves HTTP/3 to IPv6 and IPv4 clients on one port. The listener rejects other keys
Changed
- The HTTP/3 listener opens one UDP socket and one receiver per online scheduler (
receivers), not one. With one receiver the socket dropped datagrams under load, and static files over HTTP/3 served less than half the requests per second - A request with the method
QUERYreaches the handler withmethod => query. Before, the method was<<"QUERY">> nhttp_limits:from_opts/1returns every limit. A limit thatOptsdoes not set has the value fromdefault_limits/0- An HTTP/1.1 connection builds its parser options and limits once, parses each request head once, and checks the header limits in one pass. A keep-alive
GETcosts about 2% less CPU - With traces off, a request on any protocol builds no span attributes. With metrics also off, it does not read the clock
- Requires
nhttp_lib2.1.1. Automatic response compression matches thecontent-typemedia type about 10 times faster per response
Fixed
- Response compression does not compress a body that the handler already encoded with
content-encoding. Before, abrbody sent to a client that accepts gzip went out with two codings in the wrong order - Response compression does not change a response whose
cache-controlholdsno-transform, or a 1xx, 204, 206 or 304 response
[1.3.0] - 2026-10-19
Added
h2_dispatch => inline | worker: wherehandle_request/2runs on HTTP/2. Defaultinlinetimeouts => #{hibernate => timeout()}: the quiet time after which a connection with nothing in flight hibernates. Default 1000 ms.0hibernates on every quiet moment,infinitynever
Changed
- HTTP/2 runs
handle_request/2in the connection process, in stream order, as HTTP/1.1 and HTTP/3 do.h2_dispatch => workerkeeps one worker per stream - The acceptor does not wait for the connection process to start. A handler
init/1that returns{error, Reason}logshandler_init_failedand ends the connection with{shutdown, {handler_init, Reason}} - A connection hibernates after the
hibernatetimeout of quiet, not after every request or WebSocket frame. The idle timeout still fires on time: the quiet wait consumes part of the idle budget and the hibernation timer gets the rest - Requires
nhttp_lib2.0.0 - WebSocket frames on HTTP/1.1, HTTP/2 and HTTP/3 are decoded with
nhttp_ws:feed/2. The decoder keeps the partial frame, so the server holds no WebSocket buffer of its own - A WebSocket session on HTTP/1.1 reads with
{active, 100}instead of{active, once}, and sends the replies to all frames of one read in one socket write. A 64 B echo costs about 30% fewer reductions - A WebSocket session checks which optional
handle_ws_*callbacks the handler exports once, when it opens. A callback added by a code reload applies to sessions opened after the reload
Fixed
- Under load, new connections waited seconds in the accept queue behind the handler
init/1and the tracker call of the previous connection - HTTP/3: a WebSocket frame or push chunk that QUIC flow control refuses waits per stream and leaves in order on
stream_writable. A Close+FIN waits in the same queue. Before, it was dropped and the send returnedok - A WebSocket handler reply that holds a CLOSE ends the session, and the frames after it are not sent. A reply or close with a reserved close code, a reason over 123 bytes or a control payload over 125 bytes closes the session with 1011
- HTTP/1.1:
nhttp_ws:send/2on a closed socket returns{error, gone}, not{error, closed} - The
opts()type declares the request limit options (max_body_size,max_headers,max_header_size,max_header_name_length,max_header_value_length,max_uri_length). The listener already read them, but a caller that set one failed dialyzer get_port/1,get_port/2andget_ports/1took about one second to return the TCP port on an idle listener, because they waited for an acceptor to leaveaccept. The server now reads the port that the listener registers at startup
[1.2.1] - 2026-09-30
Changed
- Requires
nhttp_lib1.2.1
Fixed
- A
{stream, Spec, State}result with an invalidSpecon HTTP/2 ended the whole connection. The server now resets only that stream
[1.2.0] - 2026-09-28
Added
- Listener options
h2_initial_window_sizeandh2_max_frame_size, aliases of theh2_settingskeys - Listener option
h2_response_delay, a fixed or uniform delay before the response headers of a{reply, _, _}result on HTTP/2 - Listener options
h2_connection_window_policyandh2_stream_window_policy, credit policies for the HTTP/2 receive windows with the shapeseager,{threshold, N},{delay, Ms}andnever - The
on_responseshape of both credit policies, which sends the credit for a request ahead of the HEADERS of its{reply, _, _}response, and the listener optionh2_credit_batchthat batches the connection credit
Changed
- Updates
nhttp_libreference - Response bodies, producer chunks and WebSocket frames on HTTP/2 go through the codec send queue. The server keeps no remainder buffer of its own, and one connection grant reaches every blocked stream, one frame per stream per turn
- Every RST_STREAM the server sends goes through the codec, which closes the stream and purges the octets it holds for it
h2_settingsacceptsmax_send_bufferandmax_queued_streams, the bounds of the codec send queue. The server defaults both toinfinity. A response body, producer chunk or WebSocket frame that the bound refuses ends its stream with RST_STREAM(ENHANCE_YOUR_CALM). A producer sees{error, closed}and a WebSocket session closes with{transport, send_buffer_full}
Fixed
- A WebSocket-over-HTTP/2 frame that outran the send window was dropped without a record
- One large response was able to take a whole connection credit ahead of smaller ones
- The NO_ERROR RST_STREAM that asks a client to stop sending a request body went out before the response was complete, and the buffered body then followed it on a closed stream (RFC 9113 Section 5.1 and Section 8.1)
[1.1.1] - 2026-09-10
Changed
- Requires
nhttp_lib1.1.1
[1.1.0] - 2026-09-10
Changed
- Requires
nhttp_lib1.1.0 - Socket re-arming for the next read is centralized. A
setoptserror on a peer that closed ends the HTTP/1.1, HTTP/2, and WebSocket loops as a normal close - The
nhttp.http.request.durationmetric records native time units. The consumer converts them
Fixed
nhttp:start_error/0includes{already_started, pid()}
[1.0.4] - 2026-08-24
Fixed
- HTTP/1.1 connections no longer crash when the peer closes the socket. A
setoptscall that gives an error now closes the connection normally. A body read that gives an error tells the handler that the peer closed the connection
[1.0.3] - 2026-08-20
Changed
- Requires
nhttp_lib1.0.4 for correct HTTP/1.1Content-Lengthhandling. An empty-body response now emitsContent-Length: 0, a 1xx, 204, or 304 response emits noContent-Lengthfield, and a 2xx response to aCONNECTrequest can suppress it (RFC 9110 Section 8.6)
[1.0.2] - 2026-08-13
Added
- Support for Erlang/OTP 29.
[1.0.1] - 2026-08-12
Fixed
- WebSocket fragmented messages on HTTP/1.1. The connection keeps decoder state across reads, so a message split over several frames is reassembled and delivered once (RFC 6455 section 5.4)
- WebSocket CLOSE codes for decode errors. A text payload or CLOSE reason that is not valid UTF-8 gives 1007, a message over
max_message_sizegives 1009, and other framing errors keep 1002. Before, every decode error gave 1002
Changed
- Requires
nhttp_lib1.0.3 for the stateful frame decoder and incremental UTF-8 validation
Added
- WebSocket RFC 6455 compliance tests with Autobahn. Run them with
make ws-compliance
[1.0.0] - 2026-06-01
Initial public release.
Added
- HTTP/1.1 and HTTP/2 server with automatic protocol selection via TLS ALPN (RFC 9112, RFC 9113)
- HTTP/3 server over QUIC via in-process
nquiclibrary mode (RFC 9114) - Handler behaviour (
nhttp_handler) with a small callback set for request handling, streaming, and WebSocket sessions - WebSocket support across HTTP/1.1, HTTP/2, and HTTP/3 with one callback set (RFC 6455, RFC 8441, RFC 9220)
- Streaming responses via producer funs with end-to-end backpressure
- Streaming request bodies with handler-driven backpressure
- Server-Sent Events helpers (
nhttp_sse) - CORS preflight and response header helpers (
nhttp_cors) - Response compression (gzip / deflate) with MIME-aware defaults
- PROXY protocol v1/v2 for HAProxy / AWS NLB deployments
- SNI with static lookup table and dynamic callback
- OpenTelemetry spans and metrics (opt-in)
- Graceful shutdown with connection draining
- One process per connection, with HTTP/2 and HTTP/3 streams multiplexed inside it
- Per-stream handler workers on HTTP/2 to isolate slow handlers