From 7d8400f5650cf34ad1d0dd7a4d62927a2c432830 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Mon, 27 Jul 2026 02:53:49 +0200 Subject: [PATCH 1/4] test(http2): expand granular parity coverage --- test-parity/node-suite/http2/README.md | 180 +++++++++++++++++- .../http2/compat/request-properties.ts | 32 ++++ .../http2/connect/invalid-authority.ts | 9 + .../http2/connect/listener-overload.ts | 22 +++ .../http2/connect/unsupported-protocol.ts | 9 + .../http2/constants/header-aliases.ts | 7 + .../http2/constants/representative-values.ts | 18 ++ .../http2/exports/compat-descriptors.ts | 24 +++ .../http2/exports/exact-module-keys.ts | 3 + .../http2/exports/function-metadata.ts | 16 ++ .../http2/exports/performServerHandshake.ts | 7 +- .../node-suite/http2/exports/request-class.ts | 12 ++ .../http2/exports/response-class.ts | 12 ++ .../exports/response-receiver-validation.ts | 9 + .../http2/exports/server-client-surface.ts | 18 -- .../headers/invalid-request-pseudoheader.ts | 24 +++ .../http2/headers/sensitive-symbol.ts | 12 ++ .../node-suite/http2/module-exports.ts | 43 ----- .../http2/plaintext/loopback-streams.ts | 111 ----------- .../node-suite/http2/plaintext/multiplex.ts | 33 ++++ .../http2/plaintext/request-body.ts | 31 +++ .../http2/plaintext/request-pseudoheaders.ts | 32 ++++ .../http2/plaintext/response-body.ts | 31 +++ .../http2/plaintext/response-headers.ts | 30 +++ .../http2/server/create-overloads.ts | 10 + .../http2/server/empty-secure-options.ts | 9 - .../node-suite/http2/server/inheritance.ts | 8 + .../http2/server/invalid-options.ts | 9 + .../http2/server/invalid-settings-option.ts | 9 + .../node-suite/http2/server/listen-address.ts | 15 ++ .../node-suite/http2/server/node-prefix.ts | 73 ------- .../http2/session/aborted-request-signal.ts | 34 ++++ .../node-suite/http2/session/class-names.ts | 20 ++ .../http2/session/client-close-callback.ts | 22 +++ .../http2/session/client-connect-state.ts | 24 +++ .../http2/session/client-destroy-state.ts | 17 ++ .../node-suite/http2/session/controls.ts | 118 ------------ .../http2/session/goaway-opaque-data.ts | 40 ++++ .../http2/session/method-surface.ts | 44 +++++ .../node-suite/http2/session/ping-echo.ts | 26 +++ .../node-suite/http2/session/ref-unref.ts | 17 ++ .../session/request-option-validation.ts | 34 ++++ .../session/sequential-session-cleanup.ts | 117 +++++++----- .../http2/session/settings-callback.ts | 25 +++ .../http2/session/settings-shape.ts | 28 +++ .../http2/settings/default-freshness.ts | 7 + .../http2/settings/default-shape.ts | 6 + .../http2/settings/invalid-boolean-values.ts | 11 ++ .../http2/settings/invalid-numeric-values.ts | 17 ++ .../http2/settings/numeric-boundaries.ts | 18 ++ .../http2/settings/packed-custom.ts | 5 + .../http2/settings/packed-defaults.ts | 3 + .../node-suite/http2/settings/packed-empty.ts | 4 + .../http2/settings/packed-header-alias.ts | 16 ++ .../node-suite/http2/settings/packed-order.ts | 11 ++ .../http2/settings/packed-unknown-key.ts | 3 + .../http2/settings/settings-helpers.ts | 22 --- .../http2/settings/unpacked-aliases.ts | 5 + .../http2/settings/unpacked-custom.ts | 5 + .../http2/settings/unpacked-invalid-length.ts | 10 + .../http2/settings/unpacked-invalid-type.ts | 9 + .../http2/settings/unpacked-typed-array.ts | 4 + .../http2/settings/unpacked-validation.ts | 14 ++ .../http2/stream/close-validation.ts | 25 +++ .../node-suite/http2/stream/initial-state.ts | 42 ++++ .../node-suite/http2/stream/sent-headers.ts | 33 ++++ .../http2/stream/trailer-surface.ts | 28 +++ test-parity/node_suite_baseline.json | 6 +- 68 files changed, 1276 insertions(+), 452 deletions(-) create mode 100644 test-parity/node-suite/http2/compat/request-properties.ts create mode 100644 test-parity/node-suite/http2/connect/invalid-authority.ts create mode 100644 test-parity/node-suite/http2/connect/listener-overload.ts create mode 100644 test-parity/node-suite/http2/connect/unsupported-protocol.ts create mode 100644 test-parity/node-suite/http2/constants/header-aliases.ts create mode 100644 test-parity/node-suite/http2/constants/representative-values.ts create mode 100644 test-parity/node-suite/http2/exports/compat-descriptors.ts create mode 100644 test-parity/node-suite/http2/exports/exact-module-keys.ts create mode 100644 test-parity/node-suite/http2/exports/function-metadata.ts create mode 100644 test-parity/node-suite/http2/exports/request-class.ts create mode 100644 test-parity/node-suite/http2/exports/response-class.ts create mode 100644 test-parity/node-suite/http2/exports/response-receiver-validation.ts delete mode 100644 test-parity/node-suite/http2/exports/server-client-surface.ts create mode 100644 test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts create mode 100644 test-parity/node-suite/http2/headers/sensitive-symbol.ts delete mode 100644 test-parity/node-suite/http2/module-exports.ts delete mode 100644 test-parity/node-suite/http2/plaintext/loopback-streams.ts create mode 100644 test-parity/node-suite/http2/plaintext/multiplex.ts create mode 100644 test-parity/node-suite/http2/plaintext/request-body.ts create mode 100644 test-parity/node-suite/http2/plaintext/request-pseudoheaders.ts create mode 100644 test-parity/node-suite/http2/plaintext/response-body.ts create mode 100644 test-parity/node-suite/http2/plaintext/response-headers.ts create mode 100644 test-parity/node-suite/http2/server/create-overloads.ts delete mode 100644 test-parity/node-suite/http2/server/empty-secure-options.ts create mode 100644 test-parity/node-suite/http2/server/inheritance.ts create mode 100644 test-parity/node-suite/http2/server/invalid-options.ts create mode 100644 test-parity/node-suite/http2/server/invalid-settings-option.ts create mode 100644 test-parity/node-suite/http2/server/listen-address.ts delete mode 100644 test-parity/node-suite/http2/server/node-prefix.ts create mode 100644 test-parity/node-suite/http2/session/aborted-request-signal.ts create mode 100644 test-parity/node-suite/http2/session/class-names.ts create mode 100644 test-parity/node-suite/http2/session/client-close-callback.ts create mode 100644 test-parity/node-suite/http2/session/client-connect-state.ts create mode 100644 test-parity/node-suite/http2/session/client-destroy-state.ts delete mode 100644 test-parity/node-suite/http2/session/controls.ts create mode 100644 test-parity/node-suite/http2/session/goaway-opaque-data.ts create mode 100644 test-parity/node-suite/http2/session/method-surface.ts create mode 100644 test-parity/node-suite/http2/session/ping-echo.ts create mode 100644 test-parity/node-suite/http2/session/ref-unref.ts create mode 100644 test-parity/node-suite/http2/session/request-option-validation.ts create mode 100644 test-parity/node-suite/http2/session/settings-callback.ts create mode 100644 test-parity/node-suite/http2/session/settings-shape.ts create mode 100644 test-parity/node-suite/http2/settings/default-freshness.ts create mode 100644 test-parity/node-suite/http2/settings/default-shape.ts create mode 100644 test-parity/node-suite/http2/settings/invalid-boolean-values.ts create mode 100644 test-parity/node-suite/http2/settings/invalid-numeric-values.ts create mode 100644 test-parity/node-suite/http2/settings/numeric-boundaries.ts create mode 100644 test-parity/node-suite/http2/settings/packed-custom.ts create mode 100644 test-parity/node-suite/http2/settings/packed-defaults.ts create mode 100644 test-parity/node-suite/http2/settings/packed-empty.ts create mode 100644 test-parity/node-suite/http2/settings/packed-header-alias.ts create mode 100644 test-parity/node-suite/http2/settings/packed-order.ts create mode 100644 test-parity/node-suite/http2/settings/packed-unknown-key.ts delete mode 100644 test-parity/node-suite/http2/settings/settings-helpers.ts create mode 100644 test-parity/node-suite/http2/settings/unpacked-aliases.ts create mode 100644 test-parity/node-suite/http2/settings/unpacked-custom.ts create mode 100644 test-parity/node-suite/http2/settings/unpacked-invalid-length.ts create mode 100644 test-parity/node-suite/http2/settings/unpacked-invalid-type.ts create mode 100644 test-parity/node-suite/http2/settings/unpacked-typed-array.ts create mode 100644 test-parity/node-suite/http2/settings/unpacked-validation.ts create mode 100644 test-parity/node-suite/http2/stream/close-validation.ts create mode 100644 test-parity/node-suite/http2/stream/initial-state.ts create mode 100644 test-parity/node-suite/http2/stream/sent-headers.ts create mode 100644 test-parity/node-suite/http2/stream/trailer-surface.ts diff --git a/test-parity/node-suite/http2/README.md b/test-parity/node-suite/http2/README.md index dd9bf142a3..dc541b9452 100644 --- a/test-parity/node-suite/http2/README.md +++ b/test-parity/node-suite/http2/README.md @@ -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: +`ffb546784c0092a9c8224d50c3ab7665dd1bcc5c253379eda034961b79f6ceca`. + +Perry ran the 57-fixture focused suite three times. The two appended fixtures +and the two fixtures changed during final 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. diff --git a/test-parity/node-suite/http2/compat/request-properties.ts b/test-parity/node-suite/http2/compat/request-properties.ts new file mode 100644 index 0000000000..36026c4b25 --- /dev/null +++ b/test-parity/node-suite/http2/compat/request-properties.ts @@ -0,0 +1,32 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + const received = new Promise((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((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + const request = client.request({ ":method": "PATCH", ":path": "/compat" }); + request.resume(); + request.end(); + await received; +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/connect/invalid-authority.ts b/test-parity/node-suite/http2/connect/invalid-authority.ts new file mode 100644 index 0000000000..334d18cbb6 --- /dev/null +++ b/test-parity/node-suite/http2/connect/invalid-authority.ts @@ -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); + } +} diff --git a/test-parity/node-suite/http2/connect/listener-overload.ts b/test-parity/node-suite/http2/connect/listener-overload.ts new file mode 100644 index 0000000000..db0ba2a428 --- /dev/null +++ b/test-parity/node-suite/http2/connect/listener-overload.ts @@ -0,0 +1,22 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("session", () => {}); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + await new Promise((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((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/connect/unsupported-protocol.ts b/test-parity/node-suite/http2/connect/unsupported-protocol.ts new file mode 100644 index 0000000000..9eadeab3d3 --- /dev/null +++ b/test-parity/node-suite/http2/connect/unsupported-protocol.ts @@ -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); + } +} diff --git a/test-parity/node-suite/http2/constants/header-aliases.ts b/test-parity/node-suite/http2/constants/header-aliases.ts new file mode 100644 index 0000000000..8128a360d7 --- /dev/null +++ b/test-parity/node-suite/http2/constants/header-aliases.ts @@ -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); diff --git a/test-parity/node-suite/http2/constants/representative-values.ts b/test-parity/node-suite/http2/constants/representative-values.ts new file mode 100644 index 0000000000..963144963a --- /dev/null +++ b/test-parity/node-suite/http2/constants/representative-values.ts @@ -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]); +} diff --git a/test-parity/node-suite/http2/exports/compat-descriptors.ts b/test-parity/node-suite/http2/exports/compat-descriptors.ts new file mode 100644 index 0000000000..7f813c7ee3 --- /dev/null +++ b/test-parity/node-suite/http2/exports/compat-descriptors.ts @@ -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, + ); + } +} diff --git a/test-parity/node-suite/http2/exports/exact-module-keys.ts b/test-parity/node-suite/http2/exports/exact-module-keys.ts new file mode 100644 index 0000000000..040dee7891 --- /dev/null +++ b/test-parity/node-suite/http2/exports/exact-module-keys.ts @@ -0,0 +1,3 @@ +import http2 from "node:http2"; + +console.log(Object.keys(http2).join(",")); diff --git a/test-parity/node-suite/http2/exports/function-metadata.ts b/test-parity/node-suite/http2/exports/function-metadata.ts new file mode 100644 index 0000000000..d090733570 --- /dev/null +++ b/test-parity/node-suite/http2/exports/function-metadata.ts @@ -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); +} diff --git a/test-parity/node-suite/http2/exports/performServerHandshake.ts b/test-parity/node-suite/http2/exports/performServerHandshake.ts index 880d1528b6..f105638c4c 100644 --- a/test-parity/node-suite/http2/exports/performServerHandshake.ts +++ b/test-parity/node-suite/http2/exports/performServerHandshake.ts @@ -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); diff --git a/test-parity/node-suite/http2/exports/request-class.ts b/test-parity/node-suite/http2/exports/request-class.ts new file mode 100644 index 0000000000..45091eb173 --- /dev/null +++ b/test-parity/node-suite/http2/exports/request-class.ts @@ -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(","), +); diff --git a/test-parity/node-suite/http2/exports/response-class.ts b/test-parity/node-suite/http2/exports/response-class.ts new file mode 100644 index 0000000000..9d299f3670 --- /dev/null +++ b/test-parity/node-suite/http2/exports/response-class.ts @@ -0,0 +1,12 @@ +import { Http2ServerResponse } from "node:http2"; +import { Stream } from "node:stream"; + +console.log("name:", Http2ServerResponse.name); +console.log("length:", Http2ServerResponse.length); +console.log("stream:", Http2ServerResponse.prototype instanceof Stream); +console.log( + "methods:", + ["setHeader", "writeHead", "write", "end"].map((key) => + typeof (Http2ServerResponse.prototype as any)[key] + ).join(","), +); diff --git a/test-parity/node-suite/http2/exports/response-receiver-validation.ts b/test-parity/node-suite/http2/exports/response-receiver-validation.ts new file mode 100644 index 0000000000..c316dc1ee1 --- /dev/null +++ b/test-parity/node-suite/http2/exports/response-receiver-validation.ts @@ -0,0 +1,9 @@ +import { Http2ServerResponse } from "node:http2"; + +for (const method of ["setHeader", "getHeader", "removeHeader"] as const) { + try { + (Http2ServerResponse.prototype[method] as any).call({}, "x-test", "yes"); + } catch (error: any) { + console.log(method, error.name, error.code); + } +} diff --git a/test-parity/node-suite/http2/exports/server-client-surface.ts b/test-parity/node-suite/http2/exports/server-client-surface.ts deleted file mode 100644 index 8cb96bf050..0000000000 --- a/test-parity/node-suite/http2/exports/server-client-surface.ts +++ /dev/null @@ -1,18 +0,0 @@ -// #3905: node:http2 server/client/handshake/sensitiveHeaders export surface. -import http2Default from "node:http2"; -import * as http2 from "node:http2"; -import { - connect, - createServer, - performServerHandshake, - sensitiveHeaders, -} from "node:http2"; - -console.log("connect:", typeof connect, connect.length); -console.log("createServer:", typeof createServer, createServer.length); -console.log("performServerHandshake:", typeof performServerHandshake); -console.log("sensitiveHeaders:", typeof sensitiveHeaders); -console.log("namespace connect:", typeof http2.connect); -console.log("default:", typeof http2Default); -console.log("default.connect:", typeof http2Default.connect); -console.log("default.createServer:", typeof http2Default.createServer); diff --git a/test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts b/test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts new file mode 100644 index 0000000000..d2a5d1f604 --- /dev/null +++ b/test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts @@ -0,0 +1,24 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + for (const key of [":status", ":protocol", ":unknown"]) { + try { + const request = client.request({ [key]: "value" }); + console.log(key, "accepted"); + request.destroy(); + } catch (error: any) { + console.log(key, error.name, error.code); + } + } +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/headers/sensitive-symbol.ts b/test-parity/node-suite/http2/headers/sensitive-symbol.ts new file mode 100644 index 0000000000..d2fa8cc9ea --- /dev/null +++ b/test-parity/node-suite/http2/headers/sensitive-symbol.ts @@ -0,0 +1,12 @@ +import * as http2 from "node:http2"; + +const descriptor = Object.getOwnPropertyDescriptor(http2, "sensitiveHeaders"); +console.log("symbol:", typeof http2.sensitiveHeaders); +console.log("global:", Symbol.keyFor(http2.sensitiveHeaders)); +console.log("description:", http2.sensitiveHeaders.description); +console.log( + "descriptor:", + descriptor?.enumerable, + descriptor?.writable, + descriptor?.configurable, +); diff --git a/test-parity/node-suite/http2/module-exports.ts b/test-parity/node-suite/http2/module-exports.ts deleted file mode 100644 index 58927a9ff0..0000000000 --- a/test-parity/node-suite/http2/module-exports.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as http2 from "node:http2"; -import { constants, sensitiveHeaders } from "node:http2"; - -const moduleKeys = Object.keys(http2); -const constantKeys = Object.keys(constants); -const sensitiveDescriptor = Object.getOwnPropertyDescriptor(http2, "sensitiveHeaders"); - -console.log("module has constants:", moduleKeys.includes("constants")); -console.log("module has sensitiveHeaders:", moduleKeys.includes("sensitiveHeaders")); -console.log("sensitive typeof:", typeof http2.sensitiveHeaders); -console.log("sensitive import same:", sensitiveHeaders === http2.sensitiveHeaders); -console.log("sensitive stable:", http2.sensitiveHeaders === http2.sensitiveHeaders); -console.log("sensitive string:", String(http2.sensitiveHeaders)); -console.log("sensitive description:", (http2.sensitiveHeaders as any).description); -console.log("sensitive keyFor:", String(Symbol.keyFor(http2.sensitiveHeaders))); -console.log( - "sensitive descriptor:", - sensitiveDescriptor?.enumerable, - sensitiveDescriptor?.writable, -); -console.log("constants key count:", constantKeys.length); -console.log("constants first five:", constantKeys.slice(0, 5).join(",")); -console.log("constants last five:", constantKeys.slice(-5).join(",")); -console.log("DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS:", constants.DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS); -console.log("DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL:", constants.DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL); -console.log("MAX_MAX_FRAME_SIZE:", constants.MAX_MAX_FRAME_SIZE); -console.log("MIN_MAX_FRAME_SIZE:", constants.MIN_MAX_FRAME_SIZE); -console.log("MAX_INITIAL_WINDOW_SIZE:", constants.MAX_INITIAL_WINDOW_SIZE); -console.log("NGHTTP2_ERR_FRAME_SIZE_ERROR:", constants.NGHTTP2_ERR_FRAME_SIZE_ERROR); -console.log("NGHTTP2_STREAM_STATE_CLOSED:", constants.NGHTTP2_STREAM_STATE_CLOSED); -console.log("NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:", constants.NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL); -console.log("NGHTTP2_DEFAULT_WEIGHT:", constants.NGHTTP2_DEFAULT_WEIGHT); -console.log("NGHTTP2_FLAG_PADDED:", constants.NGHTTP2_FLAG_PADDED); -console.log("PADDING_STRATEGY_CALLBACK:", constants.PADDING_STRATEGY_CALLBACK); -console.log("HTTP2_HEADER_TE:", constants.HTTP2_HEADER_TE); -console.log("HTTP2_HEADER_CONTENT_DISPOSITION:", constants.HTTP2_HEADER_CONTENT_DISPOSITION); -console.log("HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN:", constants.HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN); -console.log("HTTP2_HEADER_X_FORWARDED_FOR:", constants.HTTP2_HEADER_X_FORWARDED_FOR); -console.log("HTTP2_METHOD_MKCALENDAR:", constants.HTTP2_METHOD_MKCALENDAR); -console.log("HTTP2_METHOD_VERSION_CONTROL:", constants.HTTP2_METHOD_VERSION_CONTROL); -console.log("HTTP_STATUS_TOO_EARLY:", constants.HTTP_STATUS_TOO_EARLY); -console.log("HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS:", constants.HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS); -console.log("HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED:", constants.HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED); diff --git a/test-parity/node-suite/http2/plaintext/loopback-streams.ts b/test-parity/node-suite/http2/plaintext/loopback-streams.ts deleted file mode 100644 index 679010aa15..0000000000 --- a/test-parity/node-suite/http2/plaintext/loopback-streams.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as http2 from "node:http2"; - -const server = http2.createServer(); -let closed = false; -let activeClient: any; -let clientConnected = false; -let serverSessionLine = ""; -let clientFlowStarted = false; - -function closeBoth(client: any) { - if (closed) { - return; - } - closed = true; - client.close(() => console.log("client close cb")); - server.close(() => console.log("server close cb")); -} - -function maybeStartClientFlow() { - if (clientFlowStarted || !clientConnected || !serverSessionLine) { - return; - } - clientFlowStarted = true; - const client = activeClient; - console.log(serverSessionLine); - console.log("client session:", client.type, client.encrypted, client.connecting); - console.log( - "client session props:", - typeof client.localSettings, - typeof client.remoteSettings, - typeof client.state, - typeof client.socket, - ); - console.log("client request typeof:", typeof client.request); - - const req = client.request({ ":path": "/probe?x=1", ":method": "GET" }); - console.log( - "client stream initial:", - typeof req.id, - req.pending, - req.closed, - req.destroyed, - typeof req.session, - ); - console.log("client stream helpers:", typeof req.close, typeof req.setTimeout, typeof req.priority); - - let body = ""; - req.on("response", (headers: any) => { - console.log("client response:", headers[":status"], headers["x-probe"]); - }); - req.setEncoding("utf8"); - req.on("data", (chunk: string) => { - body += chunk; - }); - req.on("end", () => { - console.log("client body:", body); - console.log("client stream end state:", req.closed, req.destroyed); - closeBoth(client); - }); - req.end(); -} - -server.on("session", (session: any) => { - serverSessionLine = `server session: ${session.type} ${session.encrypted} ${session.alpnProtocol}`; - maybeStartClientFlow(); -}); - -server.on("request", (req: any, res: any) => { - console.log("request:", req.method, req.url, req.httpVersion, req.headers[":path"]); - console.log("response end typeof:", typeof res.end); -}); - -server.on("stream", (stream: any, headers: any) => { - console.log( - "stream:", - headers[":method"], - headers[":path"], - typeof stream.id, - stream.pending, - stream.closed, - stream.destroyed, - ); - console.log("stream session type:", stream.session.type); - console.log( - "stream helpers:", - typeof stream.respond, - typeof stream.end, - typeof stream.close, - typeof stream.setTimeout, - typeof stream.priority, - ); - stream.respond({ ":status": 201, "x-probe": "yes" }); - console.log("stream headersSent/sent:", stream.headersSent, stream.sentHeaders[":status"]); - stream.end("hello h2"); -}); - -server.listen(0, "127.0.0.1", () => { - console.log("listen port typeof:", typeof server.address().port); - const client = http2.connect(`http://127.0.0.1:${server.address().port}`); - activeClient = client; - - client.on("connect", () => { - clientConnected = true; - maybeStartClientFlow(); - }); - - client.on("error", (err: any) => { - console.log("client error:", err && err.code ? err.code : err && err.message); - closeBoth(client); - }); -}); diff --git a/test-parity/node-suite/http2/plaintext/multiplex.ts b/test-parity/node-suite/http2/plaintext/multiplex.ts new file mode 100644 index 0000000000..6cdf62fce8 --- /dev/null +++ b/test-parity/node-suite/http2/plaintext/multiplex.ts @@ -0,0 +1,33 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + const paths: string[] = []; + server.on("stream", (stream: any, headers: any) => { + paths.push(headers[":path"]); + stream.respond({ ":status": 204 }); + stream.end(); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await Promise.all( + ["/one", "/two"].map((path) => + new Promise((resolve, reject) => { + const request = client.request({ ":path": path }); + request.on("error", reject); + request.on("end", resolve); + request.resume(); + request.end(); + }) + ), + ); + console.log(paths.sort().join(",")); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/plaintext/request-body.ts b/test-parity/node-suite/http2/plaintext/request-body.ts new file mode 100644 index 0000000000..7cca46b595 --- /dev/null +++ b/test-parity/node-suite/http2/plaintext/request-body.ts @@ -0,0 +1,31 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + const received = new Promise((resolve) => { + server.on("request", (request: any, response: any) => { + let body = ""; + request.setEncoding("utf8"); + request.on("data", (chunk: string) => body += chunk); + request.on("end", () => { + console.log(body); + response.statusCode = 204; + response.end(); + resolve(); + }); + }); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + const request = client.request({ ":method": "POST" }); + request.end("hello h2"); + await received; +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/plaintext/request-pseudoheaders.ts b/test-parity/node-suite/http2/plaintext/request-pseudoheaders.ts new file mode 100644 index 0000000000..d52c810c25 --- /dev/null +++ b/test-parity/node-suite/http2/plaintext/request-pseudoheaders.ts @@ -0,0 +1,32 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + const received = new Promise((resolve) => { + server.on("stream", (stream: any, headers: any) => { + console.log( + headers[":method"], + headers[":path"], + headers[":scheme"], + typeof headers[":authority"], + ); + stream.respond({ ":status": 204 }); + stream.end(); + resolve(); + }); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + const request = client.request({ ":method": "PUT", ":path": "/items?x=1" }); + request.resume(); + request.end(); + await received; +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/plaintext/response-body.ts b/test-parity/node-suite/http2/plaintext/response-body.ts new file mode 100644 index 0000000000..eff05fff3b --- /dev/null +++ b/test-parity/node-suite/http2/plaintext/response-body.ts @@ -0,0 +1,31 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("stream", (stream: any) => { + stream.respond({ ":status": 200 }); + stream.end("hello h2"); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + const request = client.request(); + let body = ""; + request.setEncoding("utf8"); + request.on("data", (chunk: string) => body += chunk); + request.on("error", reject); + request.on("end", () => { + console.log(body); + resolve(); + }); + request.end(); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/plaintext/response-headers.ts b/test-parity/node-suite/http2/plaintext/response-headers.ts new file mode 100644 index 0000000000..6045cf179c --- /dev/null +++ b/test-parity/node-suite/http2/plaintext/response-headers.ts @@ -0,0 +1,30 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("stream", (stream: any) => { + stream.respond({ ":status": 201, "x-test": "yes" }); + stream.end(); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + const request = client.request(); + request.on("error", reject); + request.on( + "response", + (headers: any) => console.log(headers[":status"], headers["x-test"]), + ); + request.on("end", resolve); + request.resume(); + request.end(); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/server/create-overloads.ts b/test-parity/node-suite/http2/server/create-overloads.ts new file mode 100644 index 0000000000..f21873777d --- /dev/null +++ b/test-parity/node-suite/http2/server/create-overloads.ts @@ -0,0 +1,10 @@ +import { createServer } from "node:http2"; + +const listener = () => {}; +const bare = createServer(); +const direct = createServer(listener); +const options = createServer({}, listener); + +console.log("bare request listeners:", bare.listenerCount("request")); +console.log("direct request listeners:", direct.listenerCount("request")); +console.log("options request listeners:", options.listenerCount("request")); diff --git a/test-parity/node-suite/http2/server/empty-secure-options.ts b/test-parity/node-suite/http2/server/empty-secure-options.ts deleted file mode 100644 index a0915a674d..0000000000 --- a/test-parity/node-suite/http2/server/empty-secure-options.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as http2 from "node:http2"; - -const bare = http2.createSecureServer({}); -console.log("http2 empty secure options typeof:", typeof bare); -bare.close(); - -const withListener = http2.createSecureServer({}, (_req: any, _res: any) => {}); -console.log("http2 empty secure options listener typeof:", typeof withListener); -withListener.close(); diff --git a/test-parity/node-suite/http2/server/inheritance.ts b/test-parity/node-suite/http2/server/inheritance.ts new file mode 100644 index 0000000000..f579f460d7 --- /dev/null +++ b/test-parity/node-suite/http2/server/inheritance.ts @@ -0,0 +1,8 @@ +import { createServer } from "node:http2"; +import { Server } from "node:net"; + +const server = createServer(); +console.log("constructor:", server.constructor.name); +console.log("net server:", server instanceof Server); +console.log("listen:", typeof server.listen); +console.log("close:", typeof server.close); diff --git a/test-parity/node-suite/http2/server/invalid-options.ts b/test-parity/node-suite/http2/server/invalid-options.ts new file mode 100644 index 0000000000..c86e4ff960 --- /dev/null +++ b/test-parity/node-suite/http2/server/invalid-options.ts @@ -0,0 +1,9 @@ +import { createServer } from "node:http2"; + +for (const value of [1, true, "test", null, Symbol("test")]) { + try { + createServer(value as any); + } catch (error: any) { + console.log(typeof value, error.name, error.code); + } +} diff --git a/test-parity/node-suite/http2/server/invalid-settings-option.ts b/test-parity/node-suite/http2/server/invalid-settings-option.ts new file mode 100644 index 0000000000..26a4ecba8f --- /dev/null +++ b/test-parity/node-suite/http2/server/invalid-settings-option.ts @@ -0,0 +1,9 @@ +import { createServer } from "node:http2"; + +for (const value of [1, true, "test", null]) { + try { + createServer({ settings: value as any }); + } catch (error: any) { + console.log(typeof value, error.name, error.code); + } +} diff --git a/test-parity/node-suite/http2/server/listen-address.ts b/test-parity/node-suite/http2/server/listen-address.ts new file mode 100644 index 0000000000..77de422e61 --- /dev/null +++ b/test-parity/node-suite/http2/server/listen-address.ts @@ -0,0 +1,15 @@ +import { createServer } from "node:http2"; + +const server = createServer(); +try { + await new Promise((resolve, reject) => { + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address() as any; + console.log(typeof address.port, address.address, address.family); + resolve(); + }); + }); +} finally { + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/server/node-prefix.ts b/test-parity/node-suite/http2/server/node-prefix.ts deleted file mode 100644 index 13ee4bcbb6..0000000000 --- a/test-parity/node-suite/http2/server/node-prefix.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { writeFileSync, readFileSync } from "node:fs"; -import { createSecureServer } from "node:http2"; - -const KEY = `-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN6SnKGIrSrgfx -NCjTUlMDnrgVmk6K2DC8hgx7eqF/R0WWjYJuPElzHkY5DnuTq9w2Ut8gvbJXx/uE -k24WV8sAW+2b2mbjkG9uw+925bUC5IjegD9+l+xDkrVWAA9stkWF2d0KOgZbf9k7 -w9tQLkNOB7mW4J9ehXTMXNkBNsnHOLJBn2diylznxJ7pePQqEzdcLLZLtXnBU9Fe -jachbmGjzfId8rJXIoR7DueWjGGs0BiJHAfNXMG8Ki12Bkr+UGbWqy6AezfUtHpP -vmLNAaigg3XDPZX2SIcJb6zcKaAULLbkBs3njvEvCfh2OHlPPiUmEDhGHQPQX+/N -x4vlOgkHAgMBAAECggEACFfV8iDBQKOkqeSkJdBoOwVA01xQE8+kBeFnqHbMOdxp -1fEZ4vs+Yjs8a6xTTZpEBxmWLqmYa5rBSckVJtEgiTPeY1RSyjw6oOt6D6Zvnuzq -sxIdKYcrB8n/SUAVqBGLQtRNL4W7y/NXRTE9mpgtss+3dIxeMkNsW3t18qFS+Zhg -TP8q984k+zl3QOz6sc5T39Unuk1g98LC2sjCXwKANzZRMBMigoGDnWgk9t86cEXM -YWmyStS89HKEDmxWQMRIc/6zw5YC9Jo0cF2OJxGtN/O+LLeeNoJcdnlSAcyYRU1Q -asJhtNkMwfMRrTVH0kQfF5X3a/aJfusiJnQBcvlVeQKBgQDF8rI8dlaQ7jNOfSoZ -FhphZe1DriFaulRA9PUwrxEb/qvRstre0Egu967ILmqoqKfufyNT4W5JnWngliN6 -S7D9cvxpW0RsUQHZXMqZp7s6kt4hAdziuyC2Wx2y6+zFHkbOwJcaULYrSNHJCPOj -cMu5TIplum+hnO9rMHKEpE0fAwKBgQC3h17rEy4uFbWPQD3fNjAi9QzIKX9wm8eD -SYekgZaHpAjrLCa8oNR6qMxU5Cpn7I3o2HegSUe29jDAr8GMp47JYRTGMHUl1Zwa -KtSGEH19sRhVUqIVW2h2/tysuaYpK1hFjPWM+KpKQFNzgt2EPf5057zE7gOHLcAL -UccMgP1crQKBgQCy4h1SaHrYZHq3LoNRwli6thrRc9YuoH4taXD+uuaSTvZE/gWv -H7hrwWcQ/mli229PJ1PspKc/HWMmE2giR669jCEwsMrHu/kYzjNE4oBfcYQNfhp4 -RzVLtlHDdFM226KPixnCLThDK35x14YdqHxiixnyzqW8/g6a5mBHIBeVswKBgQCT -y79DndGdqTvqHbj1zWScci0V8F1BqSHVd1x1vSolF5NbF9YmJ3qVQOQ0JP6FbHmn -ntNPUFQhYkdGlQNQKwuQ3s5lAFcG3ev1IrK9OABnPTu0UnRWsKMC2SGLM4I9Ozu9 -3tNL8GDqpLzPk/6h5W7KZGifSnGq5cv3EaczSZk/jQKBgAcaLGi25ozeFgK1qvuQ -WFTjLYV6KaMrGd5+NF+2a/NQsDGTZSF1egKUvE5QH5YNf37xWkqwvR3rsbenxLAG -aNYjvX+bUs4Mc/bgNkO51P9sH6YoKsuFzTTx4eR5ZS+dtfoiZMfzKkRBK4Baggrv -7S9Q3thVBhvBcz19oFN2Rmvf ------END PRIVATE KEY----- -`; - -const CERT = `-----BEGIN CERTIFICATE----- -MIIDJTCCAg2gAwIBAgIUZF3wbyk6BduDu+lEeegKd2ULMK8wDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDUyNDE3NDI1NloXDTM2MDUy -MTE3NDI1NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAjekpyhiK0q4H8TQo01JTA564FZpOitgwvIYMe3qhf0dF -lo2CbjxJcx5GOQ57k6vcNlLfIL2yV8f7hJNuFlfLAFvtm9pm45BvbsPvduW1AuSI -3oA/fpfsQ5K1VgAPbLZFhdndCjoGW3/ZO8PbUC5DTge5luCfXoV0zFzZATbJxziy -QZ9nYspc58Se6Xj0KhM3XCy2S7V5wVPRXo2nIW5ho83yHfKyVyKEew7nloxhrNAY -iRwHzVzBvCotdgZK/lBm1qsugHs31LR6T75izQGooIN1wz2V9kiHCW+s3CmgFCy2 -5AbN547xLwn4djh5Tz4lJhA4Rh0D0F/vzceL5ToJBwIDAQABo28wbTAdBgNVHQ4E -FgQU1s+brNmcdkCqkncnW6rNlJpdiP0wHwYDVR0jBBgwFoAU1s+brNmcdkCqkncn -W6rNlJpdiP0wDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH -BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAHFmvSxFCTHcqiocEHF3i0seBmNwWq40 -TtyVf9qyZYUZVqM/Z7tGDsNfNOhM+YscLs1ZTs8XzdpdYBEVyCLDYGjb4Cv6r5gS -hr+E0NQBnPuker6Rw64nzahfWYjf/Eo+7nwUbCahTbXHAs43c4m0bmL02r1NxVmv -BKGQKO/uR9Dy+3TKykNQkacKJ6oDxdTDovMUKlbwU/HlyzwK/HTm762cJfgZiMYM -uru8x9wmqogCQSAz2q6a6q/CZfn1o7S5KiWd0FzinP+50g5cSL/ob0GJ8Jge1oI5 -5rap/3DFfnTn0zfJ60U52+BVFnOIqkYT7/g5N4laGrza73tYXq7FV4s= ------END CERTIFICATE----- -`; - -const keyPath = "/tmp/perry-node-suite-http2-key.pem"; -const certPath = "/tmp/perry-node-suite-http2-cert.pem"; -writeFileSync(keyPath, KEY); -writeFileSync(certPath, CERT); - -const server = createSecureServer( - { key: readFileSync(keyPath, "utf8"), cert: readFileSync(certPath, "utf8") }, - (_req: any, res: any) => { - res.statusCode = 200; - res.end("ok"); - }, -); - -server.listen({ port: 19023, host: "127.0.0.1" }, () => { - console.log("http2 secure listening"); - server.close(() => console.log("http2 secure closed")); -}); - -setTimeout(() => {}, 1500); diff --git a/test-parity/node-suite/http2/session/aborted-request-signal.ts b/test-parity/node-suite/http2/session/aborted-request-signal.ts new file mode 100644 index 0000000000..d4e0427d38 --- /dev/null +++ b/test-parity/node-suite/http2/session/aborted-request-signal.ts @@ -0,0 +1,34 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("stream", (stream: any) => { + stream.respond({ ":status": 204 }); + stream.end(); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + const controller = new AbortController(); + controller.abort(); + await new Promise((resolve) => { + const request = client.request({}, { signal: controller.signal }); + request.on("error", (error: any) => { + console.log("error:", error.name, error.code); + resolve(); + }); + request.on("end", () => { + console.log("completed"); + resolve(); + }); + request.resume(); + request.end(); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/class-names.ts b/test-parity/node-suite/http2/session/class-names.ts new file mode 100644 index 0000000000..908a70158b --- /dev/null +++ b/test-parity/node-suite/http2/session/class-names.ts @@ -0,0 +1,20 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + const serverName = new Promise((resolve) => { + server.on("session", (session: any) => resolve(session.constructor.name)); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + console.log("client:", client.constructor.name); + console.log("server:", await serverName); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/client-close-callback.ts b/test-parity/node-suite/http2/session/client-close-callback.ts new file mode 100644 index 0000000000..2f37741d54 --- /dev/null +++ b/test-parity/node-suite/http2/session/client-close-callback.ts @@ -0,0 +1,22 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("session", () => {}); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve) => { + client.close(() => { + console.log("callback:", client.closed, client.destroyed); + resolve(); + }); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/client-connect-state.ts b/test-parity/node-suite/http2/session/client-connect-state.ts new file mode 100644 index 0000000000..d885f71dfa --- /dev/null +++ b/test-parity/node-suite/http2/session/client-connect-state.ts @@ -0,0 +1,24 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + client = http2.connect(`http://127.0.0.1:${(server.address() as any).port}`); + console.log("initial:", client.connecting, client.closed, client.destroyed); + await new Promise((resolve, reject) => { + client.on("error", reject); + client.on("connect", () => { + console.log( + "connected:", + client.connecting, + client.closed, + client.destroyed, + ); + resolve(); + }); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/client-destroy-state.ts b/test-parity/node-suite/http2/session/client-destroy-state.ts new file mode 100644 index 0000000000..c651de5b19 --- /dev/null +++ b/test-parity/node-suite/http2/session/client-destroy-state.ts @@ -0,0 +1,17 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + client.destroy(); + console.log("after destroy:", client.closed, client.destroyed); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/controls.ts b/test-parity/node-suite/http2/session/controls.ts deleted file mode 100644 index 1ce9be537c..0000000000 --- a/test-parity/node-suite/http2/session/controls.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as http2 from "node:http2"; - -const server = http2.createServer(); -let closed = false; -let watchSettings = false; -let activeClient: any; -let guard: any; - -function closeBoth(client: any) { - if (closed || !client) { - return; - } - closed = true; - client.close(); - server.close(); -} - -server.on("session", (session: any) => { - console.log( - "server controls:", - typeof session.ping, - typeof session.settings, - typeof session.goaway, - typeof session.setLocalWindowSize, - typeof session.setTimeout, - typeof session.ref, - typeof session.unref, - ); - console.log( - "server settings shape:", - session.type, - session.localSettings.headerTableSize, - session.remoteSettings.initialWindowSize, - typeof session.state.localWindowSize, - typeof session.socket, - ); - session.on("remoteSettings", (settings: any) => { - if (watchSettings) { - console.log("server remoteSettings event:", settings.initialWindowSize, settings.enablePush); - } - }); - session.on("goaway", (code: number, lastStreamID: number, opaqueData: Buffer) => { - console.log( - "server goaway event:", - code, - lastStreamID, - Buffer.isBuffer(opaqueData), - opaqueData.toString("utf8"), - ); - clearTimeout(guard); - closeBoth(activeClient); - }); -}); - -server.on("stream", (stream: any) => { - stream.respond({ ":status": 200 }); - stream.end("ok"); -}); - -server.listen(0, "127.0.0.1", () => { - console.log("listen port type:", typeof server.address().port); - const client = http2.connect(`http://127.0.0.1:${server.address().port}`); - activeClient = client; - guard = setTimeout(() => { - console.log("callback guard"); - closeBoth(client); - }, 500); - - client.on("connect", () => { - console.log( - "client controls:", - typeof client.ping, - typeof client.settings, - typeof client.goaway, - typeof client.setLocalWindowSize, - typeof client.setTimeout, - typeof client.ref, - typeof client.unref, - ); - console.log( - "client settings shape:", - client.type, - client.localSettings.headerTableSize, - client.remoteSettings.initialWindowSize, - typeof client.state.localWindowSize, - typeof client.socket, - ); - console.log( - "control returns:", - typeof client.ref(), - typeof client.unref(), - typeof client.setLocalWindowSize(131072), - client.setTimeout(0) === client, - ); - - const pingReturn = client.ping(Buffer.from("abcdefgh"), (err: any, duration: number, payload: Buffer) => { - console.log("ping cb:", err === null, typeof duration, Buffer.isBuffer(payload), payload.toString("utf8")); - watchSettings = true; - const settingsReturn = client.settings({ initialWindowSize: 65535 }, (settingsErr: any, settings: any) => { - console.log("settings cb:", settingsErr === null, settings.initialWindowSize, settings.enablePush); - const req = client.request({ ":path": "/controls", ":method": "GET" }); - req.resume(); - req.on("end", () => { - console.log("request end"); - console.log("goaway return:", typeof client.goaway(0, 0, Buffer.from("bye"))); - }); - req.end(); - }); - console.log("settings return:", typeof settingsReturn); - }); - console.log("ping return:", pingReturn); - }); - - client.on("error", (err: any) => { - console.log("client error:", err && (err.code || err.message)); - closeBoth(client); - }); -}); diff --git a/test-parity/node-suite/http2/session/goaway-opaque-data.ts b/test-parity/node-suite/http2/session/goaway-opaque-data.ts new file mode 100644 index 0000000000..d42790d080 --- /dev/null +++ b/test-parity/node-suite/http2/session/goaway-opaque-data.ts @@ -0,0 +1,40 @@ +import { Buffer } from "node:buffer"; +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + const received = new Promise((resolve) => { + server.on("session", (session: any) => { + session.on( + "goaway", + (code: number, lastStreamID: number, data: Buffer) => { + console.log(code, lastStreamID, data.toString()); + resolve(); + }, + ); + }); + }); + server.on("stream", (stream: any) => { + stream.respond({ ":status": 204 }); + stream.end(); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + const request = client.request(); + request.on("error", reject); + request.on("end", resolve); + request.resume(); + request.end(); + }); + client.goaway(0, 0, Buffer.from("bye")); + await received; +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/method-surface.ts b/test-parity/node-suite/http2/session/method-surface.ts new file mode 100644 index 0000000000..31c646d31e --- /dev/null +++ b/test-parity/node-suite/http2/session/method-surface.ts @@ -0,0 +1,44 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + const serverSurface = new Promise((resolve) => { + server.on("session", (session: any) => + resolve( + [ + "ping", + "settings", + "goaway", + "origin", + "altsvc", + "setLocalWindowSize", + "ref", + "unref", + ].map((key) => typeof session[key]).join(","), + )); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + console.log( + "client:", + [ + "request", + "ping", + "settings", + "goaway", + "setLocalWindowSize", + "ref", + "unref", + ] + .map((key) => typeof client[key]).join(","), + ); + console.log("server:", await serverSurface); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/ping-echo.ts b/test-parity/node-suite/http2/session/ping-echo.ts new file mode 100644 index 0000000000..c0a3bc5050 --- /dev/null +++ b/test-parity/node-suite/http2/session/ping-echo.ts @@ -0,0 +1,26 @@ +import { Buffer } from "node:buffer"; +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + client.ping( + Buffer.from("abcdefgh"), + (error: any, duration: number, payload: Buffer) => { + if (error) return reject(error); + console.log(typeof duration, payload.toString()); + resolve(); + }, + ); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/ref-unref.ts b/test-parity/node-suite/http2/session/ref-unref.ts new file mode 100644 index 0000000000..5f66827e4e --- /dev/null +++ b/test-parity/node-suite/http2/session/ref-unref.ts @@ -0,0 +1,17 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + console.log("unref returns self:", client.unref() === client); + console.log("ref returns self:", client.ref() === client); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/request-option-validation.ts b/test-parity/node-suite/http2/session/request-option-validation.ts new file mode 100644 index 0000000000..0a8f7c9b73 --- /dev/null +++ b/test-parity/node-suite/http2/session/request-option-validation.ts @@ -0,0 +1,34 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + for ( + const [key, value] of [ + ["endStream", 1], + ["parent", true], + ["exclusive", "yes"], + ["silent", null], + ] + ) { + try { + const request = client.request({ + ":method": "CONNECT", + ":authority": "localhost", + }, { [key]: value }); + console.log(key, "accepted"); + request.destroy(); + } catch (error: any) { + console.log(key, error.name, error.code); + } + } +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/sequential-session-cleanup.ts b/test-parity/node-suite/http2/session/sequential-session-cleanup.ts index 7728275965..2d3c60d5fa 100644 --- a/test-parity/node-suite/http2/session/sequential-session-cleanup.ts +++ b/test-parity/node-suite/http2/session/sequential-session-cleanup.ts @@ -1,67 +1,88 @@ import * as http2 from "node:http2"; -function runWarmup(): Promise { - return new Promise((resolve) => { - const server = http2.createServer(); +async function runWarmup(): Promise { + const server = http2.createServer(); + let client: any; + try { + server.on("session", () => {}); server.on("stream", (stream: any) => { stream.respond({ ":status": 204 }); stream.end(); }); - server.listen(0, "127.0.0.1", () => { - const client = http2.connect(`http://127.0.0.1:${server.address().port}`); - client.on("connect", () => { - const req = client.request({ ":path": "/warmup", ":method": "GET" }); - req.resume(); - req.on("end", () => { - client.close(() => { - server.close(() => resolve()); - }); - }); - req.end(); - }); + await new Promise((resolve, reject) => { + server.on("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + client = http2.connect( + `http://127.0.0.1:${(server.address() as any).port}`, + ); + await new Promise((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); }); - }); + await new Promise((resolve, reject) => { + const request = client.request({ ":path": "/warmup" }); + request.on("error", reject); + request.on("end", resolve); + request.resume(); + request.end(); + }); + await new Promise((resolve) => client.close(resolve)); + } finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + } } -function runProbe(): Promise { - return new Promise((resolve) => { - const server = http2.createServer(); - const order: string[] = []; - let closed = false; - - function closeBoth(client: any) { - if (closed) { - return; - } - closed = true; - client.close(() => { - server.close(() => resolve()); - }); - } - - server.on("session", (session: any) => { - order.push("server"); - session.on("remoteSettings", () => {}); - }); +async function runProbe(): Promise { + const server = http2.createServer(); + let client: any; + try { + server.on("session", () => {}); server.on("stream", (stream: any) => { stream.respond({ ":status": 200 }); stream.end("ok"); }); - server.listen(0, "127.0.0.1", () => { - const client = http2.connect(`http://127.0.0.1:${server.address().port}`); + await new Promise((resolve, reject) => { + server.on("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + client = http2.connect( + `http://127.0.0.1:${(server.address() as any).port}`, + ); + await new Promise((resolve, reject) => { + client.on("error", reject); client.on("connect", () => { - order.push("client"); - console.log("probe order:", order.join(">")); - client.settings({ initialWindowSize: 65535 }, (err: any, settings: any) => { - console.log("probe settings cb:", err === null, settings.initialWindowSize); - const req = client.request({ ":path": "/probe", ":method": "GET" }); - req.resume(); - req.on("end", () => closeBoth(client)); - req.end(); - }); + console.log("probe order: client"); + resolve(); }); }); - }); + await new Promise((resolve, reject) => { + client.settings( + { initialWindowSize: 65535 }, + (error: any, settings: any) => { + if (error) return reject(error); + console.log( + "probe settings cb:", + error === null, + settings.initialWindowSize, + ); + resolve(); + }, + ); + }); + await new Promise((resolve, reject) => { + const request = client.request({ ":path": "/probe" }); + request.on("error", reject); + request.on("end", resolve); + request.resume(); + request.end(); + }); + await new Promise((resolve) => client.close(resolve)); + } finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + } } await runWarmup(); diff --git a/test-parity/node-suite/http2/session/settings-callback.ts b/test-parity/node-suite/http2/session/settings-callback.ts new file mode 100644 index 0000000000..9c5d84c8b1 --- /dev/null +++ b/test-parity/node-suite/http2/session/settings-callback.ts @@ -0,0 +1,25 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + client.settings( + { initialWindowSize: 32768 }, + (error: any, settings: any) => { + if (error) return reject(error); + console.log(settings.initialWindowSize, client.pendingSettingsAck); + resolve(); + }, + ); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/session/settings-shape.ts b/test-parity/node-suite/http2/session/settings-shape.ts new file mode 100644 index 0000000000..25a99a28c1 --- /dev/null +++ b/test-parity/node-suite/http2/session/settings-shape.ts @@ -0,0 +1,28 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + console.log( + "local:", + client.localSettings.initialWindowSize, + client.localSettings.maxFrameSize, + ); + const remote = client.remoteSettings; + console.log( + "remote:", + remote === null + ? "null" + : `${remote.initialWindowSize} ${remote.maxFrameSize}`, + ); + console.log("pending ack:", client.pendingSettingsAck); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/settings/default-freshness.ts b/test-parity/node-suite/http2/settings/default-freshness.ts new file mode 100644 index 0000000000..2d5d6402d5 --- /dev/null +++ b/test-parity/node-suite/http2/settings/default-freshness.ts @@ -0,0 +1,7 @@ +import { getDefaultSettings } from "node:http2"; + +const first = getDefaultSettings(); +first.headerTableSize = 1; +const second = getDefaultSettings(); +console.log("distinct:", first !== second); +console.log("fresh:", second.headerTableSize); diff --git a/test-parity/node-suite/http2/settings/default-shape.ts b/test-parity/node-suite/http2/settings/default-shape.ts new file mode 100644 index 0000000000..07ba9d1018 --- /dev/null +++ b/test-parity/node-suite/http2/settings/default-shape.ts @@ -0,0 +1,6 @@ +import { getDefaultSettings } from "node:http2"; + +const settings = getDefaultSettings(); +console.log("null prototype:", Object.getPrototypeOf(settings) === null); +console.log("keys:", Object.keys(settings).join(",")); +console.log("values:", Object.values(settings).join(",")); diff --git a/test-parity/node-suite/http2/settings/invalid-boolean-values.ts b/test-parity/node-suite/http2/settings/invalid-boolean-values.ts new file mode 100644 index 0000000000..c5beb874af --- /dev/null +++ b/test-parity/node-suite/http2/settings/invalid-boolean-values.ts @@ -0,0 +1,11 @@ +import { getPackedSettings } from "node:http2"; + +for (const key of ["enablePush", "enableConnectProtocol"] as const) { + for (const value of [0, 1, null, "true"]) { + try { + getPackedSettings({ [key]: value as any }); + } catch (error: any) { + console.log(key, String(value), error.name, error.code); + } + } +} diff --git a/test-parity/node-suite/http2/settings/invalid-numeric-values.ts b/test-parity/node-suite/http2/settings/invalid-numeric-values.ts new file mode 100644 index 0000000000..1cace36cf2 --- /dev/null +++ b/test-parity/node-suite/http2/settings/invalid-numeric-values.ts @@ -0,0 +1,17 @@ +import { getPackedSettings } from "node:http2"; + +for ( + const [key, value] of [ + ["headerTableSize", -1], + ["initialWindowSize", 2 ** 31], + ["maxFrameSize", 16383], + ["maxFrameSize", 2 ** 24], + ["maxConcurrentStreams", 2 ** 32], + ] as const +) { + try { + getPackedSettings({ [key]: value }); + } catch (error: any) { + console.log(key, error.name, error.code); + } +} diff --git a/test-parity/node-suite/http2/settings/numeric-boundaries.ts b/test-parity/node-suite/http2/settings/numeric-boundaries.ts new file mode 100644 index 0000000000..444bcf19cf --- /dev/null +++ b/test-parity/node-suite/http2/settings/numeric-boundaries.ts @@ -0,0 +1,18 @@ +import { getPackedSettings } from "node:http2"; + +const cases = [ + ["headerTableSize", 0], + ["headerTableSize", 2 ** 32 - 1], + ["initialWindowSize", 0], + ["initialWindowSize", 2 ** 31 - 1], + ["maxFrameSize", 16384], + ["maxFrameSize", 2 ** 24 - 1], + ["maxConcurrentStreams", 0], + ["maxConcurrentStreams", 2 ** 32 - 1], + ["maxHeaderListSize", 0], + ["maxHeaderSize", 2 ** 32 - 1], +] as const; + +for (const [key, value] of cases) { + console.log(key, value, getPackedSettings({ [key]: value }).length); +} diff --git a/test-parity/node-suite/http2/settings/packed-custom.ts b/test-parity/node-suite/http2/settings/packed-custom.ts new file mode 100644 index 0000000000..445d3c0b9e --- /dev/null +++ b/test-parity/node-suite/http2/settings/packed-custom.ts @@ -0,0 +1,5 @@ +import { getPackedSettings } from "node:http2"; + +console.log( + getPackedSettings({ customSettings: { 9999: 301 } }).toString("hex"), +); diff --git a/test-parity/node-suite/http2/settings/packed-defaults.ts b/test-parity/node-suite/http2/settings/packed-defaults.ts new file mode 100644 index 0000000000..7126d2a3bf --- /dev/null +++ b/test-parity/node-suite/http2/settings/packed-defaults.ts @@ -0,0 +1,3 @@ +import { getDefaultSettings, getPackedSettings } from "node:http2"; + +console.log(getPackedSettings(getDefaultSettings()).toString("hex")); diff --git a/test-parity/node-suite/http2/settings/packed-empty.ts b/test-parity/node-suite/http2/settings/packed-empty.ts new file mode 100644 index 0000000000..93363f7522 --- /dev/null +++ b/test-parity/node-suite/http2/settings/packed-empty.ts @@ -0,0 +1,4 @@ +import { getPackedSettings } from "node:http2"; + +console.log("missing:", getPackedSettings().length); +console.log("empty:", getPackedSettings({}).length); diff --git a/test-parity/node-suite/http2/settings/packed-header-alias.ts b/test-parity/node-suite/http2/settings/packed-header-alias.ts new file mode 100644 index 0000000000..6c0b9c438a --- /dev/null +++ b/test-parity/node-suite/http2/settings/packed-header-alias.ts @@ -0,0 +1,16 @@ +import { getPackedSettings } from "node:http2"; + +const warnings: string[] = []; +const onWarning = (warning: Error) => warnings.push(warning.name); +process.on("warning", onWarning); +try { + console.log( + getPackedSettings({ maxHeaderSize: 1, maxHeaderListSize: 2 }).toString( + "hex", + ), + ); + await new Promise((resolve) => process.nextTick(resolve)); + console.log("warnings:", warnings.join(",")); +} finally { + process.off("warning", onWarning); +} diff --git a/test-parity/node-suite/http2/settings/packed-order.ts b/test-parity/node-suite/http2/settings/packed-order.ts new file mode 100644 index 0000000000..4d9e9c45f3 --- /dev/null +++ b/test-parity/node-suite/http2/settings/packed-order.ts @@ -0,0 +1,11 @@ +import { getPackedSettings } from "node:http2"; + +const packed = getPackedSettings({ + enableConnectProtocol: false, + maxFrameSize: 16384, + initialWindowSize: 7, + maxConcurrentStreams: 6, + enablePush: true, + headerTableSize: 5, +}); +console.log(packed.toString("hex")); diff --git a/test-parity/node-suite/http2/settings/packed-unknown-key.ts b/test-parity/node-suite/http2/settings/packed-unknown-key.ts new file mode 100644 index 0000000000..53b4813116 --- /dev/null +++ b/test-parity/node-suite/http2/settings/packed-unknown-key.ts @@ -0,0 +1,3 @@ +import { getPackedSettings } from "node:http2"; + +console.log(getPackedSettings({ unknown: 1 } as any).length); diff --git a/test-parity/node-suite/http2/settings/settings-helpers.ts b/test-parity/node-suite/http2/settings/settings-helpers.ts deleted file mode 100644 index 3e563d459d..0000000000 --- a/test-parity/node-suite/http2/settings/settings-helpers.ts +++ /dev/null @@ -1,22 +0,0 @@ -// #3654: node:http2 exposes the settings helper trio -// `getDefaultSettings`, `getPackedSettings`, and `getUnpackedSettings` -// alongside the server factories. Lock in Node's observable shape and -// round-trip behavior so the export surface can't silently regress. -import * as http2 from "node:http2"; -import { Buffer } from "node:buffer"; - -console.log("typeof getDefaultSettings:", typeof http2.getDefaultSettings); -console.log("typeof getPackedSettings:", typeof http2.getPackedSettings); -console.log("typeof getUnpackedSettings:", typeof http2.getUnpackedSettings); - -const defaults = http2.getDefaultSettings(); -console.log("defaults type:", typeof defaults); -console.log("enablePush:", defaults.enablePush); -console.log("initialWindowSize:", defaults.initialWindowSize); - -const packed = http2.getPackedSettings({ enablePush: false, initialWindowSize: 1024 }); -console.log("packed is Buffer:", Buffer.isBuffer(packed)); - -const unpacked = http2.getUnpackedSettings(packed); -console.log("round-trip enablePush:", unpacked.enablePush); -console.log("round-trip initialWindowSize:", unpacked.initialWindowSize); diff --git a/test-parity/node-suite/http2/settings/unpacked-aliases.ts b/test-parity/node-suite/http2/settings/unpacked-aliases.ts new file mode 100644 index 0000000000..22156f7ff5 --- /dev/null +++ b/test-parity/node-suite/http2/settings/unpacked-aliases.ts @@ -0,0 +1,5 @@ +import { Buffer } from "node:buffer"; +import { getUnpackedSettings } from "node:http2"; + +const settings = getUnpackedSettings(Buffer.from("000600000064", "hex")); +console.log(settings.maxHeaderSize, settings.maxHeaderListSize); diff --git a/test-parity/node-suite/http2/settings/unpacked-custom.ts b/test-parity/node-suite/http2/settings/unpacked-custom.ts new file mode 100644 index 0000000000..c65f14c569 --- /dev/null +++ b/test-parity/node-suite/http2/settings/unpacked-custom.ts @@ -0,0 +1,5 @@ +import { Buffer } from "node:buffer"; +import { getUnpackedSettings } from "node:http2"; + +const settings = getUnpackedSettings(Buffer.from("270f0000012d", "hex")); +console.log(JSON.stringify(settings)); diff --git a/test-parity/node-suite/http2/settings/unpacked-invalid-length.ts b/test-parity/node-suite/http2/settings/unpacked-invalid-length.ts new file mode 100644 index 0000000000..189831581f --- /dev/null +++ b/test-parity/node-suite/http2/settings/unpacked-invalid-length.ts @@ -0,0 +1,10 @@ +import { Buffer } from "node:buffer"; +import { getUnpackedSettings } from "node:http2"; + +for (const length of [1, 5, 7]) { + try { + getUnpackedSettings(Buffer.alloc(length)); + } catch (error: any) { + console.log(length, error.name, error.code); + } +} diff --git a/test-parity/node-suite/http2/settings/unpacked-invalid-type.ts b/test-parity/node-suite/http2/settings/unpacked-invalid-type.ts new file mode 100644 index 0000000000..3905cc8706 --- /dev/null +++ b/test-parity/node-suite/http2/settings/unpacked-invalid-type.ts @@ -0,0 +1,9 @@ +import { getUnpackedSettings } from "node:http2"; + +for (const value of [1, true, "", [], {}, null]) { + try { + getUnpackedSettings(value as any); + } catch (error: any) { + console.log(error.name, error.code); + } +} diff --git a/test-parity/node-suite/http2/settings/unpacked-typed-array.ts b/test-parity/node-suite/http2/settings/unpacked-typed-array.ts new file mode 100644 index 0000000000..81ef877b8f --- /dev/null +++ b/test-parity/node-suite/http2/settings/unpacked-typed-array.ts @@ -0,0 +1,4 @@ +import { getUnpackedSettings } from "node:http2"; + +const bytes = new Uint8Array([0, 4, 0, 0, 0, 7]); +console.log(getUnpackedSettings(bytes).initialWindowSize); diff --git a/test-parity/node-suite/http2/settings/unpacked-validation.ts b/test-parity/node-suite/http2/settings/unpacked-validation.ts new file mode 100644 index 0000000000..7b646827ae --- /dev/null +++ b/test-parity/node-suite/http2/settings/unpacked-validation.ts @@ -0,0 +1,14 @@ +import { Buffer } from "node:buffer"; +import { getUnpackedSettings } from "node:http2"; + +const packed = Buffer.from("000500000001", "hex"); +for (const validate of [false, true]) { + try { + console.log( + validate, + getUnpackedSettings(packed, { validate }).maxFrameSize, + ); + } catch (error: any) { + console.log(validate, error.name, error.code); + } +} diff --git a/test-parity/node-suite/http2/stream/close-validation.ts b/test-parity/node-suite/http2/stream/close-validation.ts new file mode 100644 index 0000000000..3f874e6a4a --- /dev/null +++ b/test-parity/node-suite/http2/stream/close-validation.ts @@ -0,0 +1,25 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + const request = client.request(); + for (const value of ["string", 1.01, -1, 2 ** 32]) { + try { + request.close(value as any); + console.log(String(value), "accepted"); + } catch (error: any) { + console.log(String(value), error.name, error.code); + } + } + request.destroy(); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/stream/initial-state.ts b/test-parity/node-suite/http2/stream/initial-state.ts new file mode 100644 index 0000000000..a503836521 --- /dev/null +++ b/test-parity/node-suite/http2/stream/initial-state.ts @@ -0,0 +1,42 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("stream", (stream: any) => { + console.log( + "server:", + typeof stream.id, + stream.pending, + stream.closed, + stream.destroyed, + stream.rstCode, + ); + stream.respond({ ":status": 204 }); + stream.end(); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + const request = client.request(); + console.log( + "client:", + typeof request.id, + request.pending, + request.closed, + request.destroyed, + request.rstCode, + ); + request.on("error", reject); + request.on("end", resolve); + request.resume(); + request.end(); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/stream/sent-headers.ts b/test-parity/node-suite/http2/stream/sent-headers.ts new file mode 100644 index 0000000000..43c4c9d962 --- /dev/null +++ b/test-parity/node-suite/http2/stream/sent-headers.ts @@ -0,0 +1,33 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("stream", (stream: any) => { + console.log("before:", stream.headersSent, stream.sentHeaders); + stream.respond({ ":status": 202, "x-test": "yes" }); + console.log( + "after:", + stream.headersSent, + stream.sentHeaders[":status"], + stream.sentHeaders["x-test"], + ); + stream.end(); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + const request = client.request(); + request.on("error", reject); + request.on("end", resolve); + request.resume(); + request.end(); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node-suite/http2/stream/trailer-surface.ts b/test-parity/node-suite/http2/stream/trailer-surface.ts new file mode 100644 index 0000000000..329cd7c9bb --- /dev/null +++ b/test-parity/node-suite/http2/stream/trailer-surface.ts @@ -0,0 +1,28 @@ +import * as http2 from "node:http2"; + +const server = http2.createServer(); +let client: any; +try { + server.on("stream", (stream: any) => { + console.log("server:", typeof stream.sendTrailers); + stream.respond({ ":status": 204 }); + stream.end(); + }); + await new Promise((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((resolve, reject) => { + client.on("error", reject); + client.on("connect", resolve); + }); + await new Promise((resolve, reject) => { + const request = client.request(); + console.log("client:", typeof request.sendTrailers); + request.on("error", reject); + request.on("end", resolve); + request.resume(); + request.end(); + }); +} finally { + client?.destroy(); + await new Promise((resolve) => server.close(() => resolve())); +} diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index b2287fc0bc..04eddba9cc 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -2,7 +2,7 @@ "_schema": { "description": "Floor baseline for scripts/node_suite_regression_check.py. Each module's run must produce pass >= floor.pass; dropping below is a regression (exit 1). Improvements are always accepted and reported as ratchet candidates. Captured in the node-26 environment with scripts/node_suite_run.py (pre-warm + fast/slow lanes).", "oracle": "node v26.5.0 (pinned by .node-version)", - "note": "Deterministic modules are floored at full pass. Timing/racy modules (http2, net, stream, diagnostics_channel, fs-promises) carry a small margin below observed pass so ordinary flake does not false-alarm; the guard still catches real regressions, which are large (e.g. dns 6->0, http 19->9). node_suite_run.normalize() scrubs environment-variant tokens (console.time hrtime durations, stack-trace frame lines) symmetrically before the stdout compare, so console is floored at full pass (119) on its deterministic content. http is verified 19/19 in isolation but the full-suite harness flakes to 17 under port contention, so it is floored at 17 (flake margin, not a regression); a real http break is a much larger drop. Floors were refreshed from a clean node-26 run at 2810/2863 (98.1%), then the deterministic child_process floor was measured independently at 43/53 on Node 26.5.0." + "note": "Deterministic modules are floored at full pass. Timing/racy modules (net, stream, diagnostics_channel, fs-promises) carry a small margin below observed pass so ordinary flake does not false-alarm; the guard still catches real regressions, which are large (e.g. dns 6->0, http 19->9). node_suite_run.normalize() scrubs environment-variant tokens (console.time hrtime durations, stack-trace frame lines) symmetrically before the stdout compare, so console is floored at full pass (119) on its deterministic content. http is verified 19/19 in isolation but the full-suite harness flakes to 17 under port contention, so it is floored at 17 (flake margin, not a regression); a real http break is a much larger drop. The granular http2 suite is stable at 32/59 with no margin. Floors were refreshed from a clean node-26 run at 2810/2863 (98.1%), then the deterministic child_process floor was measured independently at 43/53 on Node 26.5.0." }, "modules": { "assert": { @@ -82,8 +82,8 @@ "total": 19 }, "http2": { - "pass": 8, - "total": 9 + "pass": 32, + "total": 59 }, "https": { "pass": 5, From 71469d6cff9138c58db02313ab451737e3359fe0 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Mon, 27 Jul 2026 08:56:35 +0200 Subject: [PATCH 2/4] test(http2): strengthen reviewed fixtures --- test-parity/node-suite/http2/README.md | 6 +++--- .../node-suite/http2/compat/request-properties.ts | 12 ++++++++---- .../node-suite/http2/settings/numeric-boundaries.ts | 6 +++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/test-parity/node-suite/http2/README.md b/test-parity/node-suite/http2/README.md index dc541b9452..d48b8d9771 100644 --- a/test-parity/node-suite/http2/README.md +++ b/test-parity/node-suite/http2/README.md @@ -51,10 +51,10 @@ cleanup, and waited on a timer instead of a protocol event. Node 26.5.0 ran all 59 fixtures three times with no failures or output changes. Each run produced the same combined stdout digest: -`ffb546784c0092a9c8224d50c3ab7665dd1bcc5c253379eda034961b79f6ceca`. +`902c9e6a695d33df3653724f01459f346b6ed29929fed42f3fb8820db27b5747`. -Perry ran the 57-fixture focused suite three times. The two appended fixtures -and the two fixtures changed during final review then ran in isolation three +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 diff --git a/test-parity/node-suite/http2/compat/request-properties.ts b/test-parity/node-suite/http2/compat/request-properties.ts index 36026c4b25..6248d3a3a9 100644 --- a/test-parity/node-suite/http2/compat/request-properties.ts +++ b/test-parity/node-suite/http2/compat/request-properties.ts @@ -22,10 +22,14 @@ try { client.on("error", reject); client.on("connect", resolve); }); - const request = client.request({ ":method": "PATCH", ":path": "/compat" }); - request.resume(); - request.end(); - await received; + const completed = new Promise((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((resolve) => server.close(() => resolve())); diff --git a/test-parity/node-suite/http2/settings/numeric-boundaries.ts b/test-parity/node-suite/http2/settings/numeric-boundaries.ts index 444bcf19cf..9d8664bb0b 100644 --- a/test-parity/node-suite/http2/settings/numeric-boundaries.ts +++ b/test-parity/node-suite/http2/settings/numeric-boundaries.ts @@ -14,5 +14,9 @@ const cases = [ ] as const; for (const [key, value] of cases) { - console.log(key, value, getPackedSettings({ [key]: value }).length); + console.log( + key, + value, + getPackedSettings({ [key]: value }).toString("hex"), + ); } From 1a0ce0cb852b54341d37e9b92c9b3d00cef4ab7f Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 00:00:51 +0200 Subject: [PATCH 3/4] style(codegen): format loop purity match arm --- crates/perry-codegen/src/loop_purity.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/perry-codegen/src/loop_purity.rs b/crates/perry-codegen/src/loop_purity.rs index aeace93d48..364d34cb3b 100644 --- a/crates/perry-codegen/src/loop_purity.rs +++ b/crates/perry-codegen/src/loop_purity.rs @@ -121,9 +121,7 @@ fn expr_alloc_free(e: &Expr) -> bool { // Element READS never allocate — they return an existing element / a // number. Recurse so the object and index are themselves alloc-free. Expr::IndexGet { object, index } => expr_alloc_free(object) && expr_alloc_free(index), - Expr::BufferIndexGet { buffer, index } => { - expr_alloc_free(buffer) && expr_alloc_free(index) - } + Expr::BufferIndexGet { buffer, index } => expr_alloc_free(buffer) && expr_alloc_free(index), Expr::Uint8ArrayGet { array, index } => expr_alloc_free(array) && expr_alloc_free(index), // `arr[i]++` / `--`: read-modify-write of an existing numeric slot, no // growth, no allocation. From 30a6f17568c99be5219b20cc765a2aa1993ca75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 06:35:49 +0200 Subject: [PATCH 4/4] test(http2): address parity review feedback --- changelog.d/6866-http2-parity-expansion.md | 1 + .../http2/headers/invalid-request-pseudoheader.ts | 1 + .../node-suite/http2/session/request-option-validation.ts | 1 + test-parity/node-suite/http2/settings/packed-empty.ts | 4 ++-- test-parity/node_suite_baseline.json | 8 +++++++- 5 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6866-http2-parity-expansion.md diff --git a/changelog.d/6866-http2-parity-expansion.md b/changelog.d/6866-http2-parity-expansion.md new file mode 100644 index 0000000000..8b9adf4ed2 --- /dev/null +++ b/changelog.d/6866-http2-parity-expansion.md @@ -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. diff --git a/test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts b/test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts index d2a5d1f604..e518498e22 100644 --- a/test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts +++ b/test-parity/node-suite/http2/headers/invalid-request-pseudoheader.ts @@ -13,6 +13,7 @@ try { try { const request = client.request({ [key]: "value" }); console.log(key, "accepted"); + request.once("error", () => {}); request.destroy(); } catch (error: any) { console.log(key, error.name, error.code); diff --git a/test-parity/node-suite/http2/session/request-option-validation.ts b/test-parity/node-suite/http2/session/request-option-validation.ts index 0a8f7c9b73..0895e920bd 100644 --- a/test-parity/node-suite/http2/session/request-option-validation.ts +++ b/test-parity/node-suite/http2/session/request-option-validation.ts @@ -23,6 +23,7 @@ try { ":authority": "localhost", }, { [key]: value }); console.log(key, "accepted"); + request.once("error", () => {}); request.destroy(); } catch (error: any) { console.log(key, error.name, error.code); diff --git a/test-parity/node-suite/http2/settings/packed-empty.ts b/test-parity/node-suite/http2/settings/packed-empty.ts index 93363f7522..e506b6fc46 100644 --- a/test-parity/node-suite/http2/settings/packed-empty.ts +++ b/test-parity/node-suite/http2/settings/packed-empty.ts @@ -1,4 +1,4 @@ import { getPackedSettings } from "node:http2"; -console.log("missing:", getPackedSettings().length); -console.log("empty:", getPackedSettings({}).length); +console.log("missing:", getPackedSettings().toString("hex")); +console.log("empty:", getPackedSettings({}).toString("hex")); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 0404946b59..6c1e297b3a 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -89,7 +89,13 @@ }, "http2": { "pass": 32, - "total": 59 + "total": 59, + "outcomes": { + "diff": 27, + "node_err": 0, + "compile_fail": 0, + "perry_err": 0 + } }, "https": { "pass": 5,