Skip to content

SETTINGS is a constructor argument, not a negotiation: six gaps blocking a Node-compatible http2 session #87

Description

@proggeramlug

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions