0.3.0
Third review pass + h2spec interop; behaviour-visible spec fixes across the whole state machine. No breaking API change, but callers that matched on specific error atoms may see different values on edge cases (ALPN, ENABLE_PUSH, IWS).
Added
- External interop suite
test/h2_interop_SUITE.erldrives the server from h2spec. Six groups: TLS generic/HPACK, h2c plaintext generic/HPACK, small-window (forced flow-control fragmentation),--strictmode. 146/146 generic+HPACK cases pass. Skips cleanly whenh2specis not on$PATH. .github/workflows/interop.yml: CI runs h2spec v2.6.0 on every push/PR; logs uploaded on failure..github/workflows/ci.yml: now runsh2_compliance_SUITEin addition to eunit; CT logs uploaded on failure.- PING/RST_STREAM flood mitigation (RFC 9113 §10.5): per-second counters per connection, GOAWAY(ENHANCE_YOUR_CALM) on overflow.
- Extended CONNECT (RFC 8441): server opt-in via
h2:start_server(Port, #{enable_connect_protocol => true, ...})advertisesSETTINGS_ENABLE_CONNECT_PROTOCOL=1. Client usesh2:request(Conn, Headers, #{protocol => <<"websocket">>}). New errors:{error, extended_connect_disabled},{error, extended_connect_method}. Inbound: server rejects:protocolwith streamPROTOCOL_ERRORif it has not opted in. h2:start_serverhonorstransport => tcp(cleartext h2c prior-knowledge listener with gen_tcp acceptor pool).h2:connect/3top-levelverifyandcacertsoptions merged into SSL options.- Owner event
{h2, Conn, {informational, StreamId, Status, Headers}}for 1xx interim responses (excluding 101). - Send-side header validation runs before HPACK encode on
send_request,send_request_headers,send_response, andsend_trailers. - README: "Using with Ranch" and "Coexisting with HTTP/1.1" sections with code sketches.
Changed
SETTINGS_ENABLE_PUSHdefault is now 0 (was 1). Server advertises 0; inbound PUSH_PROMISE on either side is a connection PROTOCOL_ERROR (RFC 9113 §6.5.2 / §8.4).- TLS
connect/2,3requires ALPNh2. Previously fell through silently onprotocol_not_negotiated; now returns{error, alpn_not_negotiated}(§3.3). - Connection-level receive window fixed at 65535 regardless of
SETTINGS_INITIAL_WINDOW_SIZE(§6.9.2). IWS now only adjusts stream windows. - Request builder no longer injects
:authority = ""when the caller omitshost. Non-CONNECT requests without host now send no:authority; CONNECT without host returns{error, missing_authority}(§8.3.1). - Trailers from the peer transition the stream to
half_closed_remote(wasclosed), so a handler mid-response isn't surprised byinvalid_stream_statewhen the peer pipelines body+trailers (§5.1). closingstate proactively closes the TCP/TLS socket on entry (§5.4) instead of waiting up to 5 s for the peer.- Closed-stream error classification: closed-reason retained in a compact id → reason side map bounded at 10 000 entries. Late DATA/HEADERS on a recently-closed stream is scoped exactly (connection vs stream) regardless of whether the full record has been evicted from the 100-entry window.
- Owner event
{h2, Conn, {goaway, LastStreamId}}is now{goaway, LastStreamId, ErrorCode}. - Per-stream events (
data,trailers,stream_reset) routed to the registered stream handler when set; connection owner receives them only as fallback. - HEADERS whose encoded block exceeds peer
SETTINGS_MAX_FRAME_SIZEare split into HEADERS + CONTINUATION chain (§4.2). - Body-less responses (HEAD / 204 / 304) emit a trailing
{data, Sid, <<>>, true}event so callers waiting for end-of-stream don't hang. - HPACK: Huffman encode/decode tables precomputed once via
persistent_term+-on_load; dynamic table caches length and uses a singlelists:reverseon eviction.
Fixed
- HEADERS/DATA on a stream closed via END_STREAM: connection STREAM_CLOSED (was stream-scoped RST) (§5.1).
- HEADERS/DATA on a stream closed via RST_STREAM: stream-scoped STREAM_CLOSED (was connection-scoped).
- CONTINUATION without an outstanding pending HEADERS: connection PROTOCOL_ERROR (§6.10).
- PRIORITY self-dependency on both the PRIORITY frame and inline HEADERS priority: stream PROTOCOL_ERROR (§5.3.1).
- PRIORITY frame with length != 5: stream FRAME_SIZE_ERROR (was connection) (§6.3).
- WINDOW_UPDATE on an idle stream: connection PROTOCOL_ERROR (was silently ignored) (§5.1).
- Unknown frame types: ignored per §4.1 (previously function_clause-crashed the connection).
- Inbound
MAX_CONCURRENT_STREAMSenforced: peer HEADERS over our advertised limit now getRST_STREAM(REFUSED_STREAM)(§5.1.2). SETTINGS_ENABLE_PUSH=1received as client: connection PROTOCOL_ERROR (§6.5.2).SETTINGS_INITIAL_WINDOW_SIZEabove 2³¹−1: connection FLOW_CONTROL_ERROR (was PROTOCOL_ERROR) (§6.9.2).- Body-less responses (HEAD/204/304): accept
content-length > 0withEND_STREAM(RFC 9110 §9.3.2); reject whenEND_STREAMis absent on the header block. in_closed_stream_rangewas asymmetric in client mode (only matched peer-initiated ids); now mirrors server mode.- HPACK decoder: truncated literal returns
{error, incomplete_string}instead offunction_clause. - 1xx interim responses with
END_STREAMorContent-Length→ streamPROTOCOL_ERROR(§8.1, RFC 9110 §15.2). - CONNECT tunnel flag no longer pre-set on the request; the stream becomes a tunnel only when the 2xx response is sent/received. Non-2xx CONNECT responses permit trailers and enforce body rules (RFC 7540 §8.3).
:authoritycontaining userinfo (user@host) rejected withPROTOCOL_ERRORon both inbound and outbound paths (§8.3.1);check_authority_hostruns for CONNECT requests too.- Extended CONNECT
:protocolvalue validated as an RFC 7230 token (RFC 8441 §4). :schemepseudo-header follows the actual transport (httpon TCP,httpson TLS).:method = CONNECToutbound: trailers rejected with{error, tunnel_no_trailers}.WINDOW_UPDATEwith increment 0 on a non-zero stream → stream RST_STREAM(PROTOCOL_ERROR) (§6.9.1).:statusparsing: malformed values trigger streamPROTOCOL_ERRORinstead of crashing the gen_statem.:status = 101rejected on both send and receive (§8.6).- Padding counted against receive flow control (§6.1). Connection-level receive window consumed on DATA for closed or unknown streams (§5.1).
Content-Lengthenforcement (§8.1.1): duplicate/mismatched/non-numeric/negative values →PROTOCOL_ERROR; DATA overshoot or END_STREAM mismatch → stream RST.- Server-side request trailers: trailing HEADERS without END_STREAM →
PROTOCOL_ERROR. - Field name validation tightened to RFC 7230
tchar(rejects SP, HTAB, colon in regular headers, other controls, DEL, non-ASCII). Field values: leading/trailing SP/HTAB rejected in addition to NUL/CR/LF. SETTINGS_MAX_HEADER_LIST_SIZEenforced in both directions: outbound exceed →{error, header_list_too_large}; inbound exceed → streamPROTOCOL_ERROR.
Docs
docs/features.md: PRIORITY metadata is parsed and self-dep rejected, but no scheduler is implemented (RFC 9218 supersedes RFC 7540 priorities).