Changelog
View SourceAll notable changes to this project are documented here. Format loosely follows Keep a Changelog; versions follow Semantic Versioning.
[0.7.0] - 2026-06-15
Fixed
- The early-response inbound drain no longer resets the connection when the
upload outlives the drain bound. The bound was a hardcoded 5 s
lingering_timeoutthat was not even wired throughstart_server, so a large body over a slow link kept uploading when the timer fired and the server closed with unread data, sending RST and losing the response (e.g. a 413). The drain is now a configurable{MaxBytes, MaxMs}budget defaulting to{infinity, 30000}.
Added
early_response_drainlistener option: a{MaxBytes, MaxMs}budget bounding the early-response inbound drain (either component may beinfinity;0disables the drain). Default{infinity, 30000}.h1:respond/6takes a per-response options map;early_response_drainoverrides the listener budget for that response.
Changed
- The early-response drain default rose from 5 s to 30 s (no byte cap), matching
nginx's
lingering_time, so large uploads on slow links deliver the response. lingering_timeout => Msis still accepted as the time-only form ofearly_response_drain({infinity, Ms}) and is now honored bystart_server.
[0.6.2] - 2026-06-12
Fixed
- A server responding before the request body is fully read (e.g. rejecting an
oversized upload with 413) no longer resets the connection mid-upload. h1 now
adds
Connection: close, sends the response, half-closes the write side, and drains the remaining inbound body before closing, so the client receives the response. The drain is bounded by the newlingering_timeoutoption (default 5 s) and discarded bytes are not parsed, so a large leftover body cannot tripmax_body_size.
[0.6.1] - 2026-06-10
Fixed
h1:accept_upgrade/3now strips any caller-suppliedConnectionorUpgradeheaders (case-insensitive) from ExtraHeaders before adding its own, so the 101 response carries exactly one of each with h1's canonical values. Previously a caller passing them produced duplicate headers, which spec-strict WebSocket clients (Safari, undici) reject.
[0.6.0] - 2026-06-05
Added
max_header_block_sizebounds the total bytes of a message's header block (default 64 KB), configurable onstart_server/connect. It covers request and response headers and trailers. Over-limit input is rejected withheader_block_too_large, which maps to HTTP 431.
Changed
- Less per-request work on the hot paths: the body is measured without being copied, each header line is parsed in a single scan, and the response header block is built in one pass. No behaviour change.
Security
- Close an unbounded-buffer path in header parsing. A peer could otherwise grow the parse buffer without end by dribbling a header line that never terminates, or by stacking headers that each stayed under the per-field size and header-count limits. The new header-block cap bounds it.
[0.5.0] - 2026-06-05
Added
h1:respond/5sends status, headers, and body in a single socket write and ends the stream. AContent-Lengthis added when the headers carry neitherContent-LengthnorTransfer-Encoding, so a fully-known body is sent fixed-length in onegen_tcp:sendrather than the two writessend_response/4+send_data/4would do.
[0.4.0] - 2026-06-04
Changed
- Server
requestandupgradeevents now deliver the full origin-form target (path plus query) asPath; previously the query string was dropped. Behavior change to the event'sPath.
[0.3.0] - 2026-06-02
Added
- Listeners can bind a specific address or family.
start_server/2,3acceptip => inet:ip_address()(an 8-tuple selects IPv6) andinet6 => boolean()(bind the IPv6 wildcard::) for both thetcpandssltransports.
[0.2.3] - 2026-05-28
Changed
- Build and dialyzer clean on OTP 27, 28 and 29. Replaces the legacy
catch Exproperator (removed in OTP 29) withtry ... catch _:_ -> ok end, retypesupgrade_fromasgen_statem:from(), and drops a handful of unreachable clauses surfaced byunmatched_returns.
Tests / CI
- Interop suite's
docker_runnow picks the last non-empty stdout line as the container id so cold image pulls don't confuse it. - New GitHub Actions matrix runs build, xref, dialyze and tests on OTP 27, 28 and 29 (rebar3 3.27.0).
0.2.2 - 2026-05-20
Security
- Reject chunked bodies whose declared chunk size exceeds
max_body_sizebefore buffering, and cap the chunk-extension scan at 4096 bytes. Both paths previously let a peer grow the parser buffer without bound. - Enforce
max_empty_linesusing the parser's persistent counter so a peer can no longer bypass the limit by dripping one blank line per packet (bare-CRLF lines are now counted too). - Keep the socket passive after an Upgrade / CONNECT is detected until the handler accepts, so tunnel bytes are no longer re-parsed as HTTP.
recv_capsule/4now honors the overall timeout across reads and caps the partial buffer at 16 MB (capsule_too_large).- Acceptor backs off on unknown accept errors instead of spinning at 100% CPU.
Fixed
wait_connected/1,2could hang: waiters were stored with a malformed reply tag, sogen_statem:replynever reached the caller.- Server stream map leaked one closed-stream entry per keep-alive request; streams are now dropped once both directions finish.
- Chunked response framing over a non-chunked
Transfer-Encodingnow appendschunkedto the header so it matches the wire bytes. - Partial response status line returns
moreinstead ofbad_request. - Connection policy (keep-alive / close) is resolved before the
requestevent is emitted, so handlers see consistent state. - Server loop notifies the handler with
stream_resetwhen the connection dies mid-stream, preventing an orphaned handler from hanging. stop_server/1erases thepersistent_termentry created bystart_server/3.set_active_oncesynthesizes a close event when re-arming the socket fails, so the connection shuts down promptly instead of stalling.
0.2.1 - 2026-04-19
Fixed
h1:upgrade/4crash when the:pathpseudo-header is supplied (dead-code path inhandle_client_upgrade/4eagerly encoded pseudo-headers beforeupgrade_wire/1stripped them).
0.2.0 - 2026-04-19
Added
h1:accept_connect/3,4: server-side reply of200 Connection Establishedto a classic HTTP/1.1 CONNECT with atomic raw-socket handoff (RFC 9110 §9.3.6, RFC 9112 §3.2.3). Mirrorsh1:accept_upgrade/3but writes status 200 and injects no Connection/Upgrade/framing headers, so bytes past the terminating CRLF belong to the tunnel.
0.1.1 — 2026-04-19
Changed
- Hex package name is
erlang_h1(the shorth1is already taken on hex.pm). The OTP application, module atom, and public API are unchanged — call sites continue to useh1:connect/2etc.
0.1.0 — 2026-04-19
Initial release.
HTTP/1.1 core
- Streaming pure-Erlang parser covering RFC 9110 / RFC 9112: request and response lines, chunked transfer, trailers, obs-fold, 100-continue, absolute-form / asterisk / authority request targets.
- Request / response / chunk / trailer encoder with CRLF-injection guards on header names, methods, paths, and reason phrases.
- RFC 9297 capsule codec (
h1_capsule) wire-compatible with the equivalent module inerlang_h2. h1_connectiongen_statemrunning in both client and server modes with keep-alive, pipelining (in-order response delivery on the server),Expect: 100-continue, and Upgrade / 101 Switching Protocols with socket handoff.
Public API
h1module mirrors the surface ofh2andquic_h3so callers can swap protocols:connect,request,send_data,send_trailers,cancel,goaway,close,start_server,stop_server,send_response, plus H1-specificupgrade,accept_upgrade,continue,pipeline.- Event messages (
{h1, Conn, Event}) match theh2/h3shape, with an extra{upgrade, ...}/{upgraded, ...}pair for the 101 handoff.
Hardening
- Smuggling guards (RFC 9112 §6.1). Reject messages carrying both
Content-LengthandTransfer-Encoding: chunked; reject differingContent-Lengthvalues across duplicates or in a comma-list; rejectTransfer-Encodingon HTTP/1.0. - DoS guards. Chunk-size hex capped at 16 digits; configurable
max_body_sizeenforced per stream; idle and request timers armed asgen_statemtimeouts (slowloris guard). - Field validation. Encoder rejects CRLF in header names, methods,
paths, and reason phrases; parser rejects forbidden fields in
trailers per RFC 9110 §6.5.1; obs-fold re-validates the unfolded
value against
max_header_value_size. - Response framing (RFC 9110 §6.3). HEAD / 1xx / 204 / 304 responses are body-less regardless of framing headers; close-delimited bodies finalise on socket close.
- TLS defaults. Client connects with
verify_peer+ OS CA trust + hostname check + automatic SNI; user-suppliedssl_optswin on every key. - Host enforcement. Client auto-adds the
Host:header from the connect hostname; server rejects HTTP/1.1 requests missingHostwith 400 and closes the connection.
Listener + client integration
- Built-in acceptor pool + listener (
h1_acceptor,h1_listener) and per-connection server loop (h1_server) that preserves pipelined response byte order on the wire (RFC 9112 §9.3). - Client connect helper (
h1_client) drives TCP / TLS handshake, socket ownership, andwait_connectedsynchronisation. - Reference
ranch_protocolmodule + docs covering drop-in Ranch integration and ALPN multiplexing withh2.
Tests
- 52 EUnit tests + 4 PropEr roundtrip properties.
- 149 Common Test cases across parser, encoder, capsule codec,
connection state machine, end-to-end client/server, Upgrade +
capsule exchange, Ranch integration, compliance vectors
(smuggling / framing / chunked / DoS) and interop (curl, plus
python:3-alpineandnginx:alpineunder Docker).
Documentation
README.md— install, quickstart, full client & server walkthroughs, TLS guidance, tuning, events and error reference, Ranch snippet.docs/features.md— RFC coverage, in-scope vs. intentionally out-of-scope, internal module map.docs/ranch.md— production-shape protocol module, ALPN multiplex, graceful drain, gotchas.