Perry's HTTP/2 transport lane reached h2spec 147/147 against Perry's own http2.createServer on turnloop-http's http2::Connection (the hyper/h2 arm it replaces scores 146). In getting there it hit six API gaps, five of which force a host to interfere with the core's own protocol state. Filing them together because they share one cause: Connection owns SETTINGS state that only the host knows the policy for.
1. A second SETTINGS frame kills the connection on its ack
settings_awaiting_ack is a single bool. A host that sends SETTINGS while one is already outstanding — which is exactly what session.settings() means in Node, callable at any time — gets the connection torn down when the second ack arrives.
This makes session.settings() unimplementable without host interference. RFC 9113 §6.5 requires SETTINGS frames to be acknowledged in order, so the state is a queue, not a flag.
2. No event for a SETTINGS ack
Node's session.settings(obj, callback) fires its callback when the peer acknowledges, and passes the round-trip duration as its third argument. The core consumes the ack silently, so a host cannot know when to call back and has to guess or fabricate. (Perry's shim previously fired immediately with a hardcoded 0.0 duration — that is what this migration is replacing.)
3. Event::Settings is a unit variant
The frame's contents are not surfaced, so a host cannot implement 'remoteSettings' or report what the peer actually asked for. Node exposes the full settings object on both sides.
4. Event::Goaway drops the opaque data
RFC 9113 §6.8 allows arbitrary debug data on GOAWAY, and Node surfaces it as the third argument to the 'goaway' event. The core parses the frame and discards that field. A host cannot recover it.
Related detail the Perry lane measured: Node's third argument is undefined, not a zero-length Buffer, when the frame carried no opaque data — so the distinction between "absent" and "empty" is observable and needs to survive.
5. Connection::new always advertises three identifiers
The initial SETTINGS frame always carries three settings. Node's server sends an empty SETTINGS frame, measured on the wire by a sibling lane using a raw socket peer. A host that wants Node's bytes cannot get them, and the Perry lane deliberately did not work around it.
Node serialises configured keys in ascending identifier order when the application does supply settings, which is worth matching at the same time.
6. No setter for any Limits after new
Limits is fixed at construction, so a host cannot apply a settings change — its own or the peer's — to the running connection. This compounds 1–3: even with the ack plumbing, there is nowhere to put the result.
Why these belong together
Items 1, 2, 3 and 6 are one feature: SETTINGS as a live, two-sided negotiation rather than a constructor argument. 4 is independent and small. 5 is a default-value decision that should follow whatever shape 6 takes.
A host cannot work around any of 1–3 or 6 without writing its own frame encoder alongside the core's and splicing both into one byte stream — which is the same class of problem as #85's post-GOAWAY finding, where emitting was irreversible and the fix was to let the host decide.
Provenance
All six were found building Perry's real node:http2 binding (PerryTS/perry PR #10354, docs/turnloop/http2b-report.md), verified against Node 26.5.1. None is theoretical: each one blocked a specific Node-observable behaviour, and Perry's session.settings() is shipping incomplete because of 1–3 and 6.
Perry's HTTP/2 transport lane reached h2spec 147/147 against Perry's own
http2.createServeronturnloop-http'shttp2::Connection(the hyper/h2arm it replaces scores 146). In getting there it hit six API gaps, five of which force a host to interfere with the core's own protocol state. Filing them together because they share one cause:Connectionowns SETTINGS state that only the host knows the policy for.1. A second SETTINGS frame kills the connection on its ack
settings_awaiting_ackis a singlebool. A host that sends SETTINGS while one is already outstanding — which is exactly whatsession.settings()means in Node, callable at any time — gets the connection torn down when the second ack arrives.This makes
session.settings()unimplementable without host interference. RFC 9113 §6.5 requires SETTINGS frames to be acknowledged in order, so the state is a queue, not a flag.2. No event for a SETTINGS ack
Node's
session.settings(obj, callback)fires its callback when the peer acknowledges, and passes the round-trip duration as its third argument. The core consumes the ack silently, so a host cannot know when to call back and has to guess or fabricate. (Perry's shim previously fired immediately with a hardcoded0.0duration — that is what this migration is replacing.)3.
Event::Settingsis a unit variantThe frame's contents are not surfaced, so a host cannot implement
'remoteSettings'or report what the peer actually asked for. Node exposes the full settings object on both sides.4.
Event::Goawaydrops the opaque dataRFC 9113 §6.8 allows arbitrary debug data on GOAWAY, and Node surfaces it as the third argument to the
'goaway'event. The core parses the frame and discards that field. A host cannot recover it.Related detail the Perry lane measured: Node's third argument is
undefined, not a zero-length Buffer, when the frame carried no opaque data — so the distinction between "absent" and "empty" is observable and needs to survive.5.
Connection::newalways advertises three identifiersThe initial SETTINGS frame always carries three settings. Node's server sends an empty SETTINGS frame, measured on the wire by a sibling lane using a raw socket peer. A host that wants Node's bytes cannot get them, and the Perry lane deliberately did not work around it.
Node serialises configured keys in ascending identifier order when the application does supply settings, which is worth matching at the same time.
6. No setter for any
LimitsafternewLimitsis fixed at construction, so a host cannot apply a settings change — its own or the peer's — to the running connection. This compounds 1–3: even with the ack plumbing, there is nowhere to put the result.Why these belong together
Items 1, 2, 3 and 6 are one feature: SETTINGS as a live, two-sided negotiation rather than a constructor argument. 4 is independent and small. 5 is a default-value decision that should follow whatever shape 6 takes.
A host cannot work around any of 1–3 or 6 without writing its own frame encoder alongside the core's and splicing both into one byte stream — which is the same class of problem as #85's post-GOAWAY finding, where emitting was irreversible and the fix was to let the host decide.
Provenance
All six were found building Perry's real
node:http2binding (PerryTS/perry PR #10354,docs/turnloop/http2b-report.md), verified against Node 26.5.1. None is theoretical: each one blocked a specific Node-observable behaviour, and Perry'ssession.settings()is shipping incomplete because of 1–3 and 6.