Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6866-http2-parity-expansion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expanded deterministic `node:http2` parity coverage from 9 to 59 focused fixtures and protected the new fixture and outcome counts with the node-suite regression floor.
180 changes: 178 additions & 2 deletions test-parity/node-suite/http2/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,179 @@
# node:http2 granular parity suite
# `node:http2` granular parity suite

Focused deterministic cases for Perry's `node:http2` compatibility layer. Cases avoid external network dependencies and use loopback/self-closing servers where possible.
This suite contains 59 deterministic, single-contract fixtures. Network cases
use plaintext HTTP/2 on `127.0.0.1` with ephemeral ports and explicit event
barriers. Network fixtures close clients and servers in `finally`; the
sequential-cleanup regression resolves only after both close callbacks run.

## Pinned sources

- Node.js 26.5.0, commit `bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`:
`lib/http2.js`, `lib/internal/http2/{core,compat,util}.js`, and the HTTP/2
tests under `test/parallel` and `test/sequential`.
- Deno commit `34c46613cbe20450b74c0e8d4f0fd8f6f781d807`:
`ext/node/polyfills/http2.ts`, `http2_esm.ts`,
`internal/http2/{core,compat,constants,util}.ts`, and
`tests/unit_node/http2_test.ts`.
- Bun commit `44f6469e0d4ae93467aa65c7e3bc9001000c7b31`: `src/js/node/http2.ts`,
`test/js/node/http2/node-http2.test.js`, and Bun's selected copies of Node's
HTTP/2 tests.

The main Node contracts came from `test-http2-getpackedsettings.js`,
`test-http2-createserver-options.js`, `test-http2-invalidargtypes-errors.js`,
`test-http2-request-response-proto.js`, `test-http2-connect.js`,
`test-http2-client-destroy.js`, `test-http2-session-unref.js`,
`test-http2-ping.js`, `test-http2-update-settings.js`,
`test-http2-goaway-opaquedata.js`,
`test-http2-client-request-options-errors.js`,
`test-http2-misused-pseudoheaders.js`, `test-http2-sent-headers.js`,
`test-http2-multiplex.js`, and the compat request/response tests.

## Coverage

| Category | Fixtures | Contracts |
| --------- | -------: | --------------------------------------------------------------------------------------------------------- |
| settings | 17 | defaults, freshness, packing order, aliases, custom IDs, typed arrays, validation, and error codes |
| session | 13 | connect/close/destroy state, class and method surface, ref/unref, AbortSignal, settings, ping, and GOAWAY |
| exports | 7 | exact keys, descriptors, receiver checks, function metadata, handshake helper, and compat classes |
| plaintext | 5 | pseudoheaders, request/response bodies, response headers, and multiplexing |
| server | 5 | overloads, option validation, inheritance, and ephemeral listen address |
| stream | 4 | initial state, sent headers, close validation, and trailer surface |
| connect | 3 | listener overload and authority/protocol validation |
| constants | 2 | representative protocol, settings, header, method, and status values |
| headers | 2 | sensitive-header symbol and invalid request pseudoheaders |
| compat | 1 | HTTP/2 request method, URL, version, and pseudoheader mapping |

The suite replaces five broad fixtures and removes two secure-server fixtures.
The removed secure case used a fixed port, wrote fixed `/tmp` paths without
cleanup, and waited on a timer instead of a protocol event.

## Results

Node 26.5.0 ran all 59 fixtures three times with no failures or output changes.
Each run produced the same combined stdout digest:
`902c9e6a695d33df3653724f01459f346b6ed29929fed42f3fb8820db27b5747`.

Perry ran the focused suite with 57 fixtures three times. The two appended
fixtures and every fixture changed during review then ran in isolation three
times. All classifications stayed fixed, yielding:

```text
http2 32 pass / 59 total (54.2%) diff=27
```

There were no Node failures, compile failures, crashes, or timeouts. The
baseline is therefore `32/59` with no flake margin.

Deno matched Node on 54 of 59 fixtures. Its five stable differences are extra
module exports, default-settings key order, `maxHeaderSize` packing, alias
precedence/warnings, and unconditional unpacked-settings validation.

Bun matched Node on 35 of 59 fixtures. Its 24 stable differences group into
module/handshake surface, sensitive-header symbol identity, settings defaults
and packing/validation, pseudoheader validation, session state, GOAWAY stream
ID, and stream property defaults. All Deno and Bun cases exited cleanly; none
timed out.

Perry's 27 stable diffs group into:

- module keys, compat class identity, server inheritance, overload validation,
connect validation, and the sensitive-header symbol;
- custom settings, `initialWindowSize` bounds, header aliases, unpacked
validation, and default object shape;
- AbortSignal and request-option validation, live session class names, destroy
semantics, and missing `origin`/`altsvc` methods;
- invalid pseudoheaders, multiplex order, `close()` validation, and
`sentHeaders` before `respond()`.

## Per-fixture evidence

Each row names the primary Node 26.5.0 implementation or test used to select the
contract. Runtime columns compare exit code and stdout with Node.

| Fixture | Node source | Deno | Bun |
| ----------------------------------------- | -------------------------------------------------------------- | ----- | ----- |
| `compat/request-properties.ts` | `compat.js`; `test-http2-compat-serverrequest.js` | match | match |
| `connect/invalid-authority.ts` | `core.js`; `test-http2-connect.js` | match | match |
| `connect/listener-overload.ts` | `core.js`; `test-http2-connect.js` | match | match |
| `connect/unsupported-protocol.ts` | `core.js`; `test-http2-connect.js` | match | match |
| `constants/header-aliases.ts` | `util.js`; `test-http2-util.js` | match | match |
| `constants/representative-values.ts` | `util.js`; `test-http2-util.js` | match | match |
| `exports/compat-descriptors.ts` | `compat.js`; `test-http2-request-response-proto.js` | match | match |
| `exports/exact-module-keys.ts` | `lib/http2.js`; `core.js` | diff | diff |
| `exports/function-metadata.ts` | `lib/http2.js`; `core.js` | match | diff |
| `exports/performServerHandshake.ts` | `core.js`; `test-http2-perform-server-handshake.js` | match | diff |
| `exports/request-class.ts` | `compat.js`; `test-http2-request-response-proto.js` | match | match |
| `exports/response-class.ts` | `compat.js`; `test-http2-request-response-proto.js` | match | match |
| `exports/response-receiver-validation.ts` | `compat.js`; `test-http2-request-response-proto.js` | match | match |
| `headers/invalid-request-pseudoheader.ts` | `core.js`; `test-http2-misused-pseudoheaders.js` | match | diff |
| `headers/sensitive-symbol.ts` | `core.js`; `test-http2-sensitive-headers.js` | match | diff |
| `plaintext/multiplex.ts` | `test-http2-multiplex.js` | match | match |
| `plaintext/request-body.ts` | `test-http2-compat-serverrequest.js` | match | match |
| `plaintext/request-pseudoheaders.ts` | `test-http2-connect.js`; `test-http2-misused-pseudoheaders.js` | match | match |
| `plaintext/response-body.ts` | `test-http2-client-data-end.js` | match | match |
| `plaintext/response-headers.ts` | `test-http2-sent-headers.js` | match | match |
| `server/create-overloads.ts` | `core.js`; `test-http2-createserver-options.js` | match | match |
| `server/inheritance.ts` | `core.js`; `test-http2-createserver-options.js` | match | match |
| `server/invalid-options.ts` | `core.js`; `test-http2-createserver-options.js` | match | match |
| `server/invalid-settings-option.ts` | `core.js`; `test-http2-createserver-options.js` | match | match |
| `server/listen-address.ts` | `core.js`; `test-http2-server-startup.js` | match | match |
| `session/aborted-request-signal.ts` | `core.js`; `test-http2-client-request-options-errors.js` | match | match |
| `session/class-names.ts` | `core.js` | match | match |
| `session/client-close-callback.ts` | `test-http2-client-destroy.js` | match | match |
| `session/client-connect-state.ts` | `test-http2-create-client-connect.js` | match | match |
| `session/client-destroy-state.ts` | `test-http2-client-destroy.js` | match | diff |
| `session/goaway-opaque-data.ts` | `test-http2-goaway-opaquedata.js` | match | diff |
| `session/method-surface.ts` | `core.js` | match | match |
| `session/ping-echo.ts` | `test-http2-ping.js` | match | match |
| `session/ref-unref.ts` | `test-http2-session-unref.js` | match | match |
| `session/request-option-validation.ts` | `test-http2-client-request-options-errors.js` | match | match |
| `session/sequential-session-cleanup.ts` | `core.js`; `test-http2-create-client-connect.js` | match | match |
| `session/settings-callback.ts` | `test-http2-update-settings.js` | match | match |
| `session/settings-shape.ts` | `test-http2-session-settings.js` | match | diff |
| `settings/default-freshness.ts` | `util.js`; `test-http2-getpackedsettings.js` | match | match |
| `settings/default-shape.ts` | `util.js`; `test-http2-getpackedsettings.js` | diff | diff |
| `settings/invalid-boolean-values.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | match | diff |
| `settings/invalid-numeric-values.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | match | diff |
| `settings/numeric-boundaries.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | diff | diff |
| `settings/packed-custom.ts` | `util.js`; `test-http2-getpackedsettings.js` | match | diff |
| `settings/packed-defaults.ts` | `util.js`; `test-http2-getpackedsettings.js` | match | diff |
| `settings/packed-empty.ts` | `util.js`; `test-http2-getpackedsettings.js` | match | diff |
| `settings/packed-header-alias.ts` | `util.js`; `test-http2-getpackedsettings.js` | diff | diff |
| `settings/packed-order.ts` | `util.js`; `test-http2-getpackedsettings.js` | match | diff |
| `settings/packed-unknown-key.ts` | `util.js`; `test-http2-getpackedsettings.js` | match | diff |
| `settings/unpacked-aliases.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | match | match |
| `settings/unpacked-custom.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | match | diff |
| `settings/unpacked-invalid-length.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | match | diff |
| `settings/unpacked-invalid-type.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | match | diff |
| `settings/unpacked-typed-array.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | match | match |
| `settings/unpacked-validation.ts` | `util.js`; `test-http2-invalidargtypes-errors.js` | diff | diff |
| `stream/close-validation.ts` | `core.js`; `test-http2-invalidargtypes-errors.js` | match | match |
| `stream/initial-state.ts` | `core.js`; `test-http2-session-stream-state.js` | match | diff |
| `stream/sent-headers.ts` | `test-http2-sent-headers.js` | match | diff |
| `stream/trailer-surface.ts` | `core.js`; `test-http2-trailers.js` | match | match |

## Exclusions and stopping rule

- TLS, ALPN, certificates, secure-session origin behavior, and HTTP/1 fallback
belong to `tls`/`https` until those providers can act as a stable oracle.
- Internet tests, fixed ports, sleeps, scheduler races, large payloads, memory
pressure, GC, signals, inspector, tracing, and kernel-specific failures are
excluded.
- Socket ownership, backpressure, generic stream state, file-descriptor
semantics, diagnostics channels, async context, workers, and performance hooks
belong to their own suites.
- `respondWithFile` and `respondWithFD` stay out until they can test an HTTP/2
result without turning the fixture into an `fs` test or leaving a pending
response in Perry.
- Actual trailer and reset exchanges, pre-connect request queueing, ORIGIN, and
ALTSVC stay out where Perry cannot yet reach a deterministic completion
barrier. The suite already records their method surface or validation gap;
another timeout would add no new evidence.
- Server push and extended CONNECT are not added: push is deprecated, and the
remaining contracts either need TLS/backend work or duplicate request and
stream validation already covered here.

The audit stopped after reviewing Node's implementation and full HTTP/2 test
inventory plus Deno's and Bun's current implementations and selected suites. No
remaining case was deterministic, local, HTTP/2-specific, non-redundant, and
able to exit cleanly on all four runtimes.
36 changes: 36 additions & 0 deletions test-parity/node-suite/http2/compat/request-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as http2 from "node:http2";

const server = http2.createServer();
let client: any;
try {
const received = new Promise<void>((resolve) => {
server.on("request", (request: any, response: any) => {
console.log(
request.method,
request.url,
request.httpVersion,
request.headers[":path"],
);
response.statusCode = 204;
response.end();
resolve();
});
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
client = http2.connect(`http://127.0.0.1:${(server.address() as any).port}`);
await new Promise<void>((resolve, reject) => {
client.on("error", reject);
client.on("connect", resolve);
});
const completed = new Promise<void>((resolve, reject) => {
const request = client.request({ ":method": "PATCH", ":path": "/compat" });
request.on("error", reject);
request.on("end", resolve);
request.resume();
request.end();
});
await Promise.all([received, completed]);
} finally {
client?.destroy();
await new Promise<void>((resolve) => server.close(() => resolve()));
}
9 changes: 9 additions & 0 deletions test-parity/node-suite/http2/connect/invalid-authority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { connect } from "node:http2";

for (const value of [1, null]) {
try {
connect(value as any);
} catch (error: any) {
console.log(String(value), error.name, error.code);
}
}
22 changes: 22 additions & 0 deletions test-parity/node-suite/http2/connect/listener-overload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as http2 from "node:http2";

const server = http2.createServer();
let client: any;
try {
server.on("session", () => {});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
await new Promise<void>((resolve, reject) => {
client = http2.connect(
`http://127.0.0.1:${(server.address() as any).port}`,
{},
() => {
console.log("listener called:", client.connecting);
resolve();
},
);
client.on("error", reject);
});
} finally {
client?.destroy();
await new Promise<void>((resolve) => server.close(() => resolve()));
}
9 changes: 9 additions & 0 deletions test-parity/node-suite/http2/connect/unsupported-protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { connect } from "node:http2";

for (const protocol of ["ftp:", "file:"]) {
try {
connect(`${protocol}//localhost`);
} catch (error: any) {
console.log(protocol, error.name, error.code);
}
}
7 changes: 7 additions & 0 deletions test-parity/node-suite/http2/constants/header-aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { constants } from "node:http2";

console.log("authority:", constants.HTTP2_HEADER_AUTHORITY);
console.log("method:", constants.HTTP2_HEADER_METHOD);
console.log("path:", constants.HTTP2_HEADER_PATH);
console.log("scheme:", constants.HTTP2_HEADER_SCHEME);
console.log("status:", constants.HTTP2_HEADER_STATUS);
18 changes: 18 additions & 0 deletions test-parity/node-suite/http2/constants/representative-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { constants } from "node:http2";

for (
const key of [
"NGHTTP2_NO_ERROR",
"NGHTTP2_CANCEL",
"NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL",
"DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE",
"MIN_MAX_FRAME_SIZE",
"MAX_MAX_FRAME_SIZE",
"HTTP2_HEADER_METHOD",
"HTTP2_HEADER_STATUS",
"HTTP2_METHOD_CONNECT",
"HTTP_STATUS_OK",
] as const
) {
console.log(key, constants[key]);
}
24 changes: 24 additions & 0 deletions test-parity/node-suite/http2/exports/compat-descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";

for (
const [name, prototype, keys] of [
["request", Http2ServerRequest.prototype, ["method", "url", "headers"]],
["response", Http2ServerResponse.prototype, [
"statusCode",
"headersSent",
"sendDate",
]],
] as const
) {
for (const key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
console.log(
name,
key,
typeof descriptor?.get,
typeof descriptor?.set,
descriptor?.enumerable,
descriptor?.configurable,
);
}
}
3 changes: 3 additions & 0 deletions test-parity/node-suite/http2/exports/exact-module-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import http2 from "node:http2";

console.log(Object.keys(http2).join(","));
16 changes: 16 additions & 0 deletions test-parity/node-suite/http2/exports/function-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as http2 from "node:http2";

for (
const key of [
"connect",
"createServer",
"createSecureServer",
"getDefaultSettings",
"getPackedSettings",
"getUnpackedSettings",
"performServerHandshake",
] as const
) {
const value = http2[key];
console.log(key, typeof value, value?.name, value?.length);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
// #3720: node:http2 exposes the module-level `performServerHandshake`
// helper alongside the server factories. Lock in Node's observable
// export shape (callable, name, length) so it can't silently regress.
import * as http2 from "node:http2";

console.log("typeof:", typeof http2.performServerHandshake);
console.log("name:", http2.performServerHandshake.name);
console.log("length:", http2.performServerHandshake.length);
console.log("name:", http2.performServerHandshake?.name);
console.log("length:", http2.performServerHandshake?.length);
12 changes: 12 additions & 0 deletions test-parity/node-suite/http2/exports/request-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Http2ServerRequest } from "node:http2";
import { Readable } from "node:stream";

console.log("name:", Http2ServerRequest.name);
console.log("length:", Http2ServerRequest.length);
console.log("readable:", Http2ServerRequest.prototype instanceof Readable);
console.log(
"methods:",
["setTimeout", "destroy", "_read"].map((key) =>
typeof (Http2ServerRequest.prototype as any)[key]
).join(","),
);
Loading
Loading