diff --git a/CHANGELOG.md b/CHANGELOG.md index 36ee35300f6bce..4e74a59686da8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,8 @@ release. -26.2.0
+26.3.0
+26.2.0
26.1.0
26.0.0
diff --git a/configure.py b/configure.py index 714639bf03bf07..591b223335e645 100755 --- a/configure.py +++ b/configure.py @@ -55,7 +55,7 @@ valid_mips_float_abi = ('soft', 'hard') valid_intl_modes = ('none', 'small-icu', 'full-icu', 'system-icu') icu_versions = json.loads((tools_path / 'icu' / 'icu_versions.json').read_text(encoding='utf-8')) -maglev_enabled_architectures = ('x64', 'arm', 'arm64', 's390x') +maglev_enabled_architectures = ('x64', 'arm', 'arm64', 'ppc64', 's390x') # builtins may be removed later if they have been disabled by options shareable_builtins = {'undici/undici': 'deps/undici/undici.js', @@ -2175,7 +2175,7 @@ def configure_v8(o, configs): o['variables']['v8_promise_internal_field_count'] = 1 # Add internal field to promises for async hooks. o['variables']['v8_use_siphash'] = 0 if options.without_siphash else 1 o['variables']['v8_enable_maglev'] = B(not options.v8_disable_maglev and - flavor != 'zos' and + flavor not in ('aix', 'os400', 'zos') and o['variables']['target_arch'] in maglev_enabled_architectures) o['variables']['v8_enable_pointer_compression'] = 1 if options.enable_pointer_compression else 0 # Using the sandbox requires always allocating array buffer backing stores in the sandbox. diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 9e0433b00067c7..a9f9a148fdcc11 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -1514,7 +1514,7 @@ console.log(Buffer.isEncoding('')); diff --git a/doc/api/debugger.md b/doc/api/debugger.md index 2582750c58e4e6..965c9a5d2a4117 100644 --- a/doc/api/debugger.md +++ b/doc/api/debugger.md @@ -237,7 +237,7 @@ added: - v26.1.0 - v24.16.0 changes: - - version: REPLACEME + - version: v26.3.0 pr-url: https://github.com/nodejs/node/pull/63437 description: Add `probe_failure` terminal `error` event for inspector-side mid-session failures, and `error.details` for additional context on per-hit and terminal errors. diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 6776c790d0a433..a164ab7f35043a 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -4574,7 +4574,62 @@ throwing an error. This behavior is inconsistent with `hash.digest()` and may lead to subtle bugs. Calling `hmac.digest()` on a finalized `Hmac` instance will throw an error in a future version. +### DEP0207: `.aborted` property and `'aborted'` event in `http2` + + + +Type: Documentation-only + +Use standard stream events and state checks instead. Read-side aborts +(peer cancelled before sending `END_STREAM`) now surface as `'error'` +with code `ERR_HTTP2_STREAM_ABORTED` (clean peer reset code) or +`ERR_HTTP2_STREAM_ERROR` (non-clean code). Write-side aborts (peer +cancelled while we still had writes in flight) are detectable from +`'close'` by checking `writableFinished`. Parallels [DEP0156][] for +`http`. + +```cjs +// Deprecated +server.on('stream', (stream) => { + stream.on('aborted', () => { + // Stream was closed while the writable was still open. + }); +}); +``` + +```cjs +// Use this instead +server.on('stream', (stream) => { + // Read-side abort: peer cancelled before sending END_STREAM. + stream.on('error', (err) => { + if (err.code === 'ERR_HTTP2_STREAM_ABORTED' || + err.code === 'ERR_HTTP2_STREAM_ERROR') { + // Peer cancelled the request mid-stream. + } + }); + // Write-side abort: our response didn't fully send before close. + stream.on('close', () => { + if (!stream.writableFinished) { + // Writes were aborted (peer cancel, local destroy, etc.). + } + }); +}); +``` + +The same patterns apply to the compatibility API (`req` / `res` on +`http2.createServer((req, res) => …)`). On the read-side, errors on the +underlying stream are emitted from `req`. On the write-side you can use +`res.on('close', …)` to hear about client aborts by checking +`res.writableFinished` to confirm whether the response was written +successfully before the response closed. + [DEP0142]: #dep0142-repl_builtinlibs +[DEP0156]: #dep0156-aborted-property-and-abort-aborted-event-in-http [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3 [RFC 8247 Section 2.4]: https://www.rfc-editor.org/rfc/rfc8247#section-2.4 diff --git a/doc/api/errors.md b/doc/api/errors.md index 350a9260cdbdc0..0649fa25babe25 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1718,6 +1718,15 @@ Use of the `101` Informational status code is forbidden in HTTP/2. An invalid HTTP status code has been specified. Status codes must be an integer between `100` and `599` (inclusive). + + +### `ERR_HTTP2_STREAM_ABORTED` + +The peer reset the `Http2Stream` with a clean error code (`NGHTTP2_NO_ERROR` +or `NGHTTP2_CANCEL`) before sending `END_STREAM`, so the readable side will +not be fully delivered. Mirrors HTTP/1's `ECONNRESET` for a peer-side +`socket.destroy()`. + ### `ERR_HTTP2_STREAM_CANCEL` diff --git a/doc/api/http.md b/doc/api/http.md index ec17fe33e71e50..e7aaa04d79e299 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3680,7 +3680,7 @@ Found'`. -The `'aborted'` event is emitted whenever a `Http2Stream` instance is -abnormally aborted in mid-communication. -Its listener does not expect any arguments. +> Stability: 0 - Deprecated. Use `'close'` and `'error'` plus +> `stream.destroyed`. -The `'aborted'` event will only be emitted if the `Http2Stream` writable side -has not been ended. +Emitted when an `Http2Stream` is closed before the writable side has +been ended (via `.end()` or auto-ended via `respond({ endStream: true })`). +Listeners receive no arguments. #### Event: `'close'` @@ -1283,19 +1288,29 @@ The `'close'` event is emitted when the `Http2Stream` is destroyed. Once this event is emitted, the `Http2Stream` instance is no longer usable. The HTTP/2 error code used when closing the stream can be retrieved using -the `http2stream.rstCode` property. If the code is any value other than -`NGHTTP2_NO_ERROR` (`0`), an `'error'` event will have also been emitted. +the `http2stream.rstCode` property. #### Event: `'error'` * `error` {Error} -The `'error'` event is emitted when an error occurs during the processing of -an `Http2Stream`. +Emitted when an error occurs processing the `Http2Stream`. This includes +peer-initiated resets that arrive before the readable side has been +fully delivered: a clean reset code (`NGHTTP2_NO_ERROR` or +`NGHTTP2_CANCEL`) surfaces as [`ERR_HTTP2_STREAM_ABORTED`][], any other +code as [`ERR_HTTP2_STREAM_ERROR`][]. #### Event: `'frameError'` @@ -1378,8 +1393,8 @@ added: v8.4.0 * Type: {boolean} -Set to `true` if the `Http2Stream` instance was aborted abnormally. When set, -the `'aborted'` event will have been emitted. +`true` if the `Http2Stream` was closed while the writable side was +still open. When set, the `'aborted'` event was emitted. #### `http2stream.bufferSize` @@ -1723,6 +1738,13 @@ stream.on('push', (headers, flags) => { * `headers` {HTTP/2 Headers Object} @@ -1744,6 +1766,16 @@ req.on('response', (headers, flags) => { }); ``` +If no `'response'` listener is attached at the moment the response +arrives, the response body will be entirely discarded (the stream is +silently resumed). However, if a `'response'` listener is added, the +data from the response object **must** be consumed — either by calling +`response.read()` whenever there is a `'readable'` event, by adding a +`'data'` handler, or by calling the `.resume()` method. Until the data +is consumed, the `'end'` event will not fire. Also, until the data is +read, it will consume memory that can eventually lead to a "process +out of memory" error. + ```cjs const http2 = require('node:http2'); const client = http2.connect('https://localhost'); @@ -4035,11 +4067,8 @@ data. added: v8.4.0 --> -The `'aborted'` event is emitted whenever a `Http2ServerRequest` instance is -abnormally aborted in mid-communication. - -The `'aborted'` event will only be emitted if the `Http2ServerRequest` writable -side has not been ended. +The `'aborted'` event is emitted whenever a `Http2ServerRequest` instance +is closed while the underlying writable side is still open. #### Event: `'close'` @@ -5050,6 +5079,8 @@ you need to implement any fall-back behavior yourself. [`'unknownProtocol'`]: #event-unknownprotocol [`ClientHttp2Stream`]: #class-clienthttp2stream [`Duplex`]: stream.md#class-streamduplex +[`ERR_HTTP2_STREAM_ABORTED`]: errors.md#err_http2_stream_aborted +[`ERR_HTTP2_STREAM_ERROR`]: errors.md#err_http2_stream_error [`Http2ServerRequest`]: #class-http2http2serverrequest [`Http2ServerResponse`]: #class-http2http2serverresponse [`Http2Session` and Sockets]: #http2session-and-sockets diff --git a/doc/api/process.md b/doc/api/process.md index c054b7336a5cb6..0a7f85700ae85a 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -3171,7 +3171,7 @@ process.permission.has('fs.read'); ### `process.permission.drop(scope[, reference])` > Stability: 1.1 - Active Development diff --git a/doc/api/quic.md b/doc/api/quic.md index 9de625030fc180..91977bdaa67a26 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -926,7 +926,7 @@ A `QuicSession` represents the local side of a QUIC connection. ### `session.applicationOptions` * Type: {quic.ApplicationOptions} @@ -1047,7 +1047,7 @@ True if `session.destroy()` has been called. Read only. ### `session.localTransportParams` * Type: {quic.TransportParams|null} @@ -1351,7 +1351,7 @@ The local and remote socket addresses associated with the session. Read only. ### `session.remoteTransportParams` * Type: {quic.TransportParams|null|undefined} @@ -2319,7 +2319,7 @@ added: v23.8.0 ### `streamStats.bytesAccumulated` * Type: {bigint} @@ -2381,7 +2381,7 @@ added: v23.8.0 ### `streamStats.maxBytesAccumulated` * Type: {bigint} @@ -2437,7 +2437,7 @@ added: v23.8.0 ### type: `ApplicationOptions` * Type: {Object} @@ -2603,7 +2603,7 @@ When `true`, indicates that the endpoint should bind only to IPv6 addresses. #### `endpointOptions.reusePort` * Type: {boolean} @@ -3128,7 +3128,7 @@ to complete before timing out. #### `sessionOptions.initialRtt` * Type: {bigint|number} @@ -3365,7 +3365,7 @@ creating a session. The negotiated values can be observed via the #### `transportParams.initialSCID` * Type: {string} @@ -3378,7 +3378,7 @@ available in the `session.localTransportParams` and #### `transportParams.originalDCID` * Type: {string} @@ -3504,7 +3504,7 @@ a datagram that can be _sent_ is determined by the peer's #### `transportParams.retrySCID` * Type: {string} diff --git a/doc/api/test.md b/doc/api/test.md index 2d38f136f28332..fcb0018e9fc1b2 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -3437,7 +3437,7 @@ added: - v18.9.0 - v16.19.0 changes: - - version: REPLACEME + - version: v26.3.0 pr-url: https://github.com/nodejs/node/pull/63435 description: Added `parentId` to test events that carry a `testId`. - version: diff --git a/doc/changelogs/CHANGELOG_V26.md b/doc/changelogs/CHANGELOG_V26.md index 56812f4d072e2d..7bf48443bb33b0 100644 --- a/doc/changelogs/CHANGELOG_V26.md +++ b/doc/changelogs/CHANGELOG_V26.md @@ -8,6 +8,7 @@ +26.3.0
26.2.0
26.1.0
26.0.0
@@ -43,6 +44,176 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2026-06-01, Version 26.3.0 (Current), @aduh95 + +### Notable Changes + +#### Potential changes to macOS Universal Binary availability + +With Apple and its ecosystem progressively dropping support for Intel-based +architectures, it has become apparent that the Node.js project may not be able +to maintain the universal binaries we currently distribute for the full lifetime +of Node.js 26. This change serves to communicate that risk. At present, our +intention remains to continue shipping universal binaries supporting both Apple +Silicon and Intel-based Macs for as long as practical. + +Contributed by Antoine du Hamel in [#63055](https://github.com/nodejs/node/pull/63055). + +#### Other notable changes + +* \[[`a2a4b33dd8`](https://github.com/nodejs/node/commit/a2a4b33dd8)] - **(SEMVER-MINOR)** **buffer**: increase `Buffer.poolSize` default to 64 KiB (Matteo Collina) [#63597](https://github.com/nodejs/node/pull/63597) +* \[[`051a2152f7`](https://github.com/nodejs/node/commit/051a2152f7)] - **crypto**: update root certificates to NSS 3.123.1 (Node.js GitHub Bot) [#63527](https://github.com/nodejs/node/pull/63527) +* \[[`49462eca37`](https://github.com/nodejs/node/commit/49462eca37)] - **(SEMVER-MINOR)** **http**: add `httpValidation` option to configure header value validation (RajeshKumar11) [#61597](https://github.com/nodejs/node/pull/61597) +* \[[`97b7ab19bd`](https://github.com/nodejs/node/commit/97b7ab19bd)] - **(SEMVER-MINOR)** **inspector**: expose precise coverage start to JS runtime (sangwook) [#63079](https://github.com/nodejs/node/pull/63079) +* \[[`cfb80a2103`](https://github.com/nodejs/node/commit/cfb80a2103)] - **(SEMVER-MINOR)** **lib,permission**: add `permission.drop` (Rafael Gonzaga) [#62672](https://github.com/nodejs/node/pull/62672) + +### Commits + +* \[[`a2a4b33dd8`](https://github.com/nodejs/node/commit/a2a4b33dd8)] - **(SEMVER-MINOR)** **buffer**: increase Buffer.poolSize default to 64 KiB (Matteo Collina) [#63597](https://github.com/nodejs/node/pull/63597) +* \[[`0eff3e23b9`](https://github.com/nodejs/node/commit/0eff3e23b9)] - **build**: def `NODE_USE_NODE_CODE_CACHE` only used in node\_mksnapshot (Chengzhong Wu) [#63588](https://github.com/nodejs/node/pull/63588) +* \[[`447ab2d252`](https://github.com/nodejs/node/commit/447ab2d252)] - **build,win**: fix VS2022 arm64 PGO build (Stefan Stojanovic) [#63413](https://github.com/nodejs/node/pull/63413) +* \[[`86032758e4`](https://github.com/nodejs/node/commit/86032758e4)] - **build,win**: replace LTCG with Thin LTO for releases (Stefan Stojanovic) [#63114](https://github.com/nodejs/node/pull/63114) +* \[[`5f4d794052`](https://github.com/nodejs/node/commit/5f4d794052)] - **build,win**: add Rust toolchain automated configuration Windows (Mike McCready) [#63381](https://github.com/nodejs/node/pull/63381) +* \[[`051a2152f7`](https://github.com/nodejs/node/commit/051a2152f7)] - **crypto**: update root certificates to NSS 3.123.1 (Node.js GitHub Bot) [#63527](https://github.com/nodejs/node/pull/63527) +* \[[`d0f65e3579`](https://github.com/nodejs/node/commit/d0f65e3579)] - **crypto**: coerce -0 keylen to +0 in pbkdf2 and scrypt (Jordan Harband) [#63531](https://github.com/nodejs/node/pull/63531) +* \[[`e3ddb326c9`](https://github.com/nodejs/node/commit/e3ddb326c9)] - **crypto**: harden WebCrypto against prototype pollution (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`e04cd17dc0`](https://github.com/nodejs/node/commit/e04cd17dc0)] - **crypto**: pass CryptoKey handles to KDF jobs (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`64ba74d847`](https://github.com/nodejs/node/commit/64ba74d847)] - **crypto**: remove async from WebCrypto methods (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`bd230418b4`](https://github.com/nodejs/node/commit/bd230418b4)] - **crypto**: add WebCrypto CryptoJob mode (Filip Skokan) [#63363](https://github.com/nodejs/node/pull/63363) +* \[[`1a4090a83d`](https://github.com/nodejs/node/commit/1a4090a83d)] - **debugger**: surface inspector failures in probe mode (Joyee Cheung) [#63437](https://github.com/nodejs/node/pull/63437) +* \[[`dbc78ff825`](https://github.com/nodejs/node/commit/dbc78ff825)] - **debugger,test**: deflake resume failure test and add debug logs (Joyee Cheung) [#63524](https://github.com/nodejs/node/pull/63524) +* \[[`4da442f432`](https://github.com/nodejs/node/commit/4da442f432)] - **deps**: upgrade npm to 11.16.0 (npm team) [#63602](https://github.com/nodejs/node/pull/63602) +* \[[`63372cfa87`](https://github.com/nodejs/node/commit/63372cfa87)] - **deps**: SQLite: cherry-pick b869ed6b067d623cb1383549f2a18aa35508385d (Junsu Han) [#63525](https://github.com/nodejs/node/pull/63525) +* \[[`e286fa170d`](https://github.com/nodejs/node/commit/e286fa170d)] - **deps**: upgrade npm to 11.15.0 (npm team) [#63463](https://github.com/nodejs/node/pull/63463) +* \[[`de996437a5`](https://github.com/nodejs/node/commit/de996437a5)] - **doc**: downgrade macOS x64 to Tier 2 (Antoine du Hamel) [#63055](https://github.com/nodejs/node/pull/63055) +* \[[`22ac78750c`](https://github.com/nodejs/node/commit/22ac78750c)] - **doc**: remove duplicated sentences in large-pull-requests.md (Joyee Cheung) [#63650](https://github.com/nodejs/node/pull/63650) +* \[[`532f7f2085`](https://github.com/nodejs/node/commit/532f7f2085)] - **doc**: update `git node land` instructions for security releases (Antoine du Hamel) [#63586](https://github.com/nodejs/node/pull/63586) +* \[[`c61f90dfb9`](https://github.com/nodejs/node/commit/c61f90dfb9)] - **doc**: drop --experimental from --permission (Rafael Gonzaga) [#63583](https://github.com/nodejs/node/pull/63583) +* \[[`fd69d7b16a`](https://github.com/nodejs/node/commit/fd69d7b16a)] - **doc**: improve `fs.StatFs` properties descriptions (aymanxdev) [#62578](https://github.com/nodejs/node/pull/62578) +* \[[`693257782c`](https://github.com/nodejs/node/commit/693257782c)] - **doc**: generate llms.txt (Guilherme Araújo) [#62027](https://github.com/nodejs/node/pull/62027) +* \[[`55a57beb26`](https://github.com/nodejs/node/commit/55a57beb26)] - **doc**: explicitly ask for reproducible in JS (Rafael Gonzaga) [#63479](https://github.com/nodejs/node/pull/63479) +* \[[`4895c2babc`](https://github.com/nodejs/node/commit/4895c2babc)] - **doc**: fix URL postMessage example in worker\_threads (Kit Dallege) [#62203](https://github.com/nodejs/node/pull/62203) +* \[[`0355c36e37`](https://github.com/nodejs/node/commit/0355c36e37)] - **doc**: clarify `filter` option of `sqlite.database.applyChangeset` (Antoine du Hamel) [#63515](https://github.com/nodejs/node/pull/63515) +* \[[`c85ee22df6`](https://github.com/nodejs/node/commit/c85ee22df6)] - **doc**: fix double spaces in ERR\_TLS\_INVALID\_PROTOCOL\_METHOD (Daijiro Wachi) [#63511](https://github.com/nodejs/node/pull/63511) +* \[[`62947192f6`](https://github.com/nodejs/node/commit/62947192f6)] - **doc**: move hyperlinks outside of text blocks (Aviv Keller) [#63493](https://github.com/nodejs/node/pull/63493) +* \[[`9849690a1d`](https://github.com/nodejs/node/commit/9849690a1d)] - **doc**: edit Rust toolchain general install instructions (Antoine du Hamel) [#63488](https://github.com/nodejs/node/pull/63488) +* \[[`885d2462e9`](https://github.com/nodejs/node/commit/885d2462e9)] - **doc**: fix double space in modules.md (Daijiro Wachi) [#63512](https://github.com/nodejs/node/pull/63512) +* \[[`42fbb48bc6`](https://github.com/nodejs/node/commit/42fbb48bc6)] - **doc**: fix "options" to "option" in tls.createServer (Daijiro Wachi) [#63453](https://github.com/nodejs/node/pull/63453) +* \[[`05a7b0a301`](https://github.com/nodejs/node/commit/05a7b0a301)] - **doc**: add Rust toolchain general install instructions (Mike McCready) [#63426](https://github.com/nodejs/node/pull/63426) +* \[[`e13dfd7ed0`](https://github.com/nodejs/node/commit/e13dfd7ed0)] - **doc**: update toolchain for official releases (Richard Lau) [#63441](https://github.com/nodejs/node/pull/63441) +* \[[`82306881cc`](https://github.com/nodejs/node/commit/82306881cc)] - **doc**: fix typo in deprecations (Daijiro Wachi) [#63434](https://github.com/nodejs/node/pull/63434) +* \[[`eeb77d217c`](https://github.com/nodejs/node/commit/eeb77d217c)] - **doc,lib**: align WebCrypto names with spec (Filip Skokan) [#63518](https://github.com/nodejs/node/pull/63518) +* \[[`679e13c57f`](https://github.com/nodejs/node/commit/679e13c57f)] - **errors**: handle V8 warnings in DisallowJavascriptExecutionScope (Divyanshu Sharma) [#63491](https://github.com/nodejs/node/pull/63491) +* \[[`7f41f5d803`](https://github.com/nodejs/node/commit/7f41f5d803)] - **ffi**: validate 'void' as parameter type in getFunction and getFunctions (Anshika Jain) [#63504](https://github.com/nodejs/node/pull/63504) +* \[[`972cd227cb`](https://github.com/nodejs/node/commit/972cd227cb)] - **ffi**: remove function signature property aliases (René) [#63482](https://github.com/nodejs/node/pull/63482) +* \[[`5d7805e433`](https://github.com/nodejs/node/commit/5d7805e433)] - **ffi**: move DynamicLibrary disposer to native layer (René) [#63459](https://github.com/nodejs/node/pull/63459) +* \[[`5a0b32dc24`](https://github.com/nodejs/node/commit/5a0b32dc24)] - **gyp**: update deps gypfiles (Nad Alaba) [#63117](https://github.com/nodejs/node/pull/63117) +* \[[`49462eca37`](https://github.com/nodejs/node/commit/49462eca37)] - **(SEMVER-MINOR)** **http**: add httpValidation option to configure header value validation (RajeshKumar11) [#61597](https://github.com/nodejs/node/pull/61597) +* \[[`e3c6629ee3`](https://github.com/nodejs/node/commit/e3c6629ee3)] - **http2**: emit session close before stream close (Matteo Collina) [#63414](https://github.com/nodejs/node/pull/63414) +* \[[`97b7ab19bd`](https://github.com/nodejs/node/commit/97b7ab19bd)] - **(SEMVER-MINOR)** **inspector**: expose precise coverage start to JS runtime (sangwook) [#63079](https://github.com/nodejs/node/pull/63079) +* \[[`6bef10e7b7`](https://github.com/nodejs/node/commit/6bef10e7b7)] - **lib**: cleanup stateless diffiehellman key handling (Filip Skokan) [#62645](https://github.com/nodejs/node/pull/62645) +* \[[`fdc0b3d49c`](https://github.com/nodejs/node/commit/fdc0b3d49c)] - **lib**: define `kEnumerableProperty` atomically (Antoine du Hamel) [#63609](https://github.com/nodejs/node/pull/63609) +* \[[`99baf27aeb`](https://github.com/nodejs/node/commit/99baf27aeb)] - **lib**: fix typos in esm loader comments (RonGamzu) [#63465](https://github.com/nodejs/node/pull/63465) +* \[[`cfb80a2103`](https://github.com/nodejs/node/commit/cfb80a2103)] - **(SEMVER-MINOR)** **lib,permission**: add permission.drop (Rafael Gonzaga) [#62672](https://github.com/nodejs/node/pull/62672) +* \[[`8e75efb9bc`](https://github.com/nodejs/node/commit/8e75efb9bc)] - **meta**: flip mcollina emails in .mailmap (Matteo Collina) [#63621](https://github.com/nodejs/node/pull/63621) +* \[[`a4ae97045f`](https://github.com/nodejs/node/commit/a4ae97045f)] - **meta**: label "source maps" PRs (Chengzhong Wu) [#63591](https://github.com/nodejs/node/pull/63591) +* \[[`3455a48ae1`](https://github.com/nodejs/node/commit/3455a48ae1)] - **meta**: add `vfs` subsystem label (René) [#62331](https://github.com/nodejs/node/pull/62331) +* \[[`01bfcdfc20`](https://github.com/nodejs/node/commit/01bfcdfc20)] - **meta**: skip scheduled workflows on forks (Jamie Magee) [#63565](https://github.com/nodejs/node/pull/63565) +* \[[`bc4c457eae`](https://github.com/nodejs/node/commit/bc4c457eae)] - **meta**: add additional gitignore entries (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`e1d65d9509`](https://github.com/nodejs/node/commit/e1d65d9509)] - **module**: load ESM helpers eagerly in the snapshot (Joyee Cheung) [#63550](https://github.com/nodejs/node/pull/63550) +* \[[`6a97b0932a`](https://github.com/nodejs/node/commit/6a97b0932a)] - **quic**: add proper error codes & messages for QUIC failures (Tim Perry) [#63198](https://github.com/nodejs/node/pull/63198) +* \[[`5989f4a6e1`](https://github.com/nodejs/node/commit/5989f4a6e1)] - **quic**: support hostname verification (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`b4d30e7a78`](https://github.com/nodejs/node/commit/b4d30e7a78)] - **quic**: add stream idle timeout (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`8a1017f774`](https://github.com/nodejs/node/commit/8a1017f774)] - **quic**: add block list support for endpoints (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`5a3ab93c49`](https://github.com/nodejs/node/commit/5a3ab93c49)] - **quic**: improve peer cert verification (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`9701a82a78`](https://github.com/nodejs/node/commit/9701a82a78)] - **quic**: handle h3 max header size option (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`71788a2048`](https://github.com/nodejs/node/commit/71788a2048)] - **quic**: add rate limiting docs (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`309bd49906`](https://github.com/nodejs/node/commit/309bd49906)] - **quic**: cache timestamp for address lru cache (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`2ce5588d51`](https://github.com/nodejs/node/commit/2ce5588d51)] - **quic**: add session creation rate limiting (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`98808baed1`](https://github.com/nodejs/node/commit/98808baed1)] - **quic**: refine rate limiting (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`75a4176b32`](https://github.com/nodejs/node/commit/75a4176b32)] - **quic**: flip preferred address policy default to 'ignore' (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`8b6b03d60c`](https://github.com/nodejs/node/commit/8b6b03d60c)] - **quic**: add doc note about certificate size limitations (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`30eff873e0`](https://github.com/nodejs/node/commit/30eff873e0)] - **quic**: add applicationOptions to session (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`4303daa43c`](https://github.com/nodejs/node/commit/4303daa43c)] - **quic**: add getters for local and remote transport parameters (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`e1b1bb5465`](https://github.com/nodejs/node/commit/e1b1bb5465)] - **quic**: improve recv coalescing test sizes (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`25a416f457`](https://github.com/nodejs/node/commit/25a416f457)] - **quic**: add initial RTT option to session options (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`22e91c357f`](https://github.com/nodejs/node/commit/22e91c357f)] - **quic**: enable recvmmsg batching in Endpoint (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`c96d8a9d9b`](https://github.com/nodejs/node/commit/c96d8a9d9b)] - **quic**: improve stream header collection performance (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`409460f2ce`](https://github.com/nodejs/node/commit/409460f2ce)] - **quic**: add reusePort option to QuicEndpoint (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`9a2afffec9`](https://github.com/nodejs/node/commit/9a2afffec9)] - **quic**: coalesce received data into fewer buffers (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`f9a6a2f558`](https://github.com/nodejs/node/commit/f9a6a2f558)] - **quic**: apply multiple additional minor improvements (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`ea5f3724ee`](https://github.com/nodejs/node/commit/ea5f3724ee)] - **quic**: fix tests that are missing serverEndpoint close (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`6cffc931fc`](https://github.com/nodejs/node/commit/6cffc931fc)] - **quic**: fixup some v8:: qualifiers (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`9bc875e522`](https://github.com/nodejs/node/commit/9bc875e522)] - **quic**: fix premature unref of endpoint when listening (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`f940d6b1be`](https://github.com/nodejs/node/commit/f940d6b1be)] - **quic**: fixup UAFs in bindingdata, streams, and app (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`fd00e0acb0`](https://github.com/nodejs/node/commit/fd00e0acb0)] - **quic**: fix UAF in Application::OnTimeout() (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`378dbf00e9`](https://github.com/nodejs/node/commit/378dbf00e9)] - **quic**: improve the quic js structure (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`0045dc30b6`](https://github.com/nodejs/node/commit/0045dc30b6)] - **quic**: improve internal structure of QuicStream (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`5e38946b26`](https://github.com/nodejs/node/commit/5e38946b26)] - **quic**: add aliased struct arenas (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`95430437a0`](https://github.com/nodejs/node/commit/95430437a0)] - **quic**: add handshake timeout and default connection limits (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`5622701429`](https://github.com/nodejs/node/commit/5622701429)] - **quic**: implement rate limiting for version nego and immediate close (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`b171f391cd`](https://github.com/nodejs/node/commit/b171f391cd)] - **quic**: fixup linting issue after other changes (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`24e9f4f177`](https://github.com/nodejs/node/commit/24e9f4f177)] - **quic**: fix crash in early handshake failure (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`5025e85d0a`](https://github.com/nodejs/node/commit/5025e85d0a)] - **quic**: eliminate per-received datagram allocation (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`aec1e17ec5`](https://github.com/nodejs/node/commit/aec1e17ec5)] - **quic**: cache the timestamp on send and receive (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`9560084560`](https://github.com/nodejs/node/commit/9560084560)] - **quic**: add support for future ECN marking (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`2b3ff8ada2`](https://github.com/nodejs/node/commit/2b3ff8ada2)] - **quic**: improve batching of packet sending (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`fe3639a4d6`](https://github.com/nodejs/node/commit/fe3639a4d6)] - **quic**: improve backend quic packet processing (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`f043013d9a`](https://github.com/nodejs/node/commit/f043013d9a)] - **src**: remove TOCTOU race condition when encoding SAB-backed `Buffer`s (Antoine du Hamel) [#63517](https://github.com/nodejs/node/pull/63517) +* \[[`343958224d`](https://github.com/nodejs/node/commit/343958224d)] - **src**: skip duplicate UTF-8 validation in TextDecoder fatal path (Mert Can Altin) [#63231](https://github.com/nodejs/node/pull/63231) +* \[[`2906fa833d`](https://github.com/nodejs/node/commit/2906fa833d)] - **src**: dispatch ToV8Value(string\_view) via StringBytes::Encode (Mert Can Altin) [#63370](https://github.com/nodejs/node/pull/63370) +* \[[`860f9d8d4b`](https://github.com/nodejs/node/commit/860f9d8d4b)] - **src**: fix ContextifyContext property definer interception result (Chengzhong Wu) [#63549](https://github.com/nodejs/node/pull/63549) +* \[[`fcccffcbe6`](https://github.com/nodejs/node/commit/fcccffcbe6)] - **src**: fix crash when reading length on Storage.prototype (Mohamed Sayed) [#63529](https://github.com/nodejs/node/pull/63529) +* \[[`55f65f9fb6`](https://github.com/nodejs/node/commit/55f65f9fb6)] - **src**: improve token return value check (James M Snell) [#63483](https://github.com/nodejs/node/pull/63483) +* \[[`7a36ca46cd`](https://github.com/nodejs/node/commit/7a36ca46cd)] - **src**: expose `node::RegisterContext` to make a node managed context (Chengzhong Wu) [#62322](https://github.com/nodejs/node/pull/62322) +* \[[`9bda92963c`](https://github.com/nodejs/node/commit/9bda92963c)] - **src,sqlite**: only pass `xFilter` when user provided a callback (Antoine du Hamel) [#63516](https://github.com/nodejs/node/pull/63516) +* \[[`563db50f38`](https://github.com/nodejs/node/commit/563db50f38)] - **stream**: switch to internal `sleep` binding (Antoine du Hamel) [#63611](https://github.com/nodejs/node/pull/63611) +* \[[`a6e2322ee6`](https://github.com/nodejs/node/commit/a6e2322ee6)] - **stream**: use data listener for compose forwarding (Trivikram Kamat) [#63593](https://github.com/nodejs/node/pull/63593) +* \[[`7198895c6b`](https://github.com/nodejs/node/commit/7198895c6b)] - **stream**: serialize concurrent share consumer reads (Trivikram Kamat) [#63478](https://github.com/nodejs/node/pull/63478) +* \[[`70ba8be1d7`](https://github.com/nodejs/node/commit/70ba8be1d7)] - **stream**: fix lint error (Antoine du Hamel) [#63598](https://github.com/nodejs/node/pull/63598) +* \[[`1608d905a7`](https://github.com/nodejs/node/commit/1608d905a7)] - **stream**: reject pending reads on iterator throw (Trivikram Kamat) [#63555](https://github.com/nodejs/node/pull/63555) +* \[[`dc12b730d8`](https://github.com/nodejs/node/commit/dc12b730d8)] - **stream**: wait for push writer end fallback to drain (Trivikram Kamat) [#63503](https://github.com/nodejs/node/pull/63503) +* \[[`4f40a85a1a`](https://github.com/nodejs/node/commit/4f40a85a1a)] - **stream**: flush each fused stateless transform (Trivikram Kamat) [#63468](https://github.com/nodejs/node/pull/63468) +* \[[`526e0fc427`](https://github.com/nodejs/node/commit/526e0fc427)] - **stream**: avoid duplicate writes in toWritable (Trivikram Kamat) [#63360](https://github.com/nodejs/node/pull/63360) +* \[[`0008d01f9c`](https://github.com/nodejs/node/commit/0008d01f9c)] - **stream**: propagate abort reason in share and broadcast (Trivikram Kamat) [#63358](https://github.com/nodejs/node/pull/63358) +* \[[`217338e18b`](https://github.com/nodejs/node/commit/217338e18b)] - **stream**: fix Writable.toWeb() hang on synchronous drain (sangwook) [#61197](https://github.com/nodejs/node/pull/61197) +* \[[`381f4b1b10`](https://github.com/nodejs/node/commit/381f4b1b10)] - **stream**: disallow writing string chunk with 'buffer' encoding (René) [#63062](https://github.com/nodejs/node/pull/63062) +* \[[`cbee0de1cb`](https://github.com/nodejs/node/commit/cbee0de1cb)] - **stream**: align `Readable.toWeb` termination with eos (ikeyan) [#62394](https://github.com/nodejs/node/pull/62394) +* \[[`be91f0a927`](https://github.com/nodejs/node/commit/be91f0a927)] - **test**: shorten path in net pipe connect errors (Matteo Collina) [#63405](https://github.com/nodejs/node/pull/63405) +* \[[`83cada8bcc`](https://github.com/nodejs/node/commit/83cada8bcc)] - **test**: deflake test-debugger-probe-timeout (Joyee Cheung) [#63547](https://github.com/nodejs/node/pull/63547) +* \[[`3560b96a10`](https://github.com/nodejs/node/commit/3560b96a10)] - **test**: deflake test-webcrypto-crypto-job-mode (Filip Skokan) [#63543](https://github.com/nodejs/node/pull/63543) +* \[[`0c9c52373a`](https://github.com/nodejs/node/commit/0c9c52373a)] - **test**: remove test-node-output-v8-warning (Joyee Cheung) [#63469](https://github.com/nodejs/node/pull/63469) +* \[[`12052dbe14`](https://github.com/nodejs/node/commit/12052dbe14)] - **test**: cover webcrypto prototype pollution systematically (Filip Skokan) [#63520](https://github.com/nodejs/node/pull/63520) +* \[[`8c479f274a`](https://github.com/nodejs/node/commit/8c479f274a)] - **test**: update test426-fixtures to 9b9e225b5a63139e9a95cdd1bf874a8f0b9d131 (Node.js GitHub Bot) [#63373](https://github.com/nodejs/node/pull/63373) +* \[[`2ca32a5ee8`](https://github.com/nodejs/node/commit/2ca32a5ee8)] - **test**: update WPT for url to e4a4672e9e (Node.js GitHub Bot) [#63372](https://github.com/nodejs/node/pull/63372) +* \[[`1bf875bd21`](https://github.com/nodejs/node/commit/1bf875bd21)] - **test**: deflake async-hooks statwatcher test (Trivikram Kamat) [#63396](https://github.com/nodejs/node/pull/63396) +* \[[`97dbfa09f7`](https://github.com/nodejs/node/commit/97dbfa09f7)] - **test**: avoid test\_runner watch restart in spec snapshot (Trivikram Kamat) [#63392](https://github.com/nodejs/node/pull/63392) +* \[[`8b038d7b33`](https://github.com/nodejs/node/commit/8b038d7b33)] - **test**: reduce watch mode restart flakiness (Trivikram Kamat) [#63390](https://github.com/nodejs/node/pull/63390) +* \[[`f504c01d66`](https://github.com/nodejs/node/commit/f504c01d66)] - **test**: get rid of unnecessary `AbortController` instanciations (Antoine du Hamel) [#63489](https://github.com/nodejs/node/pull/63489) +* \[[`170585ff90`](https://github.com/nodejs/node/commit/170585ff90)] - **test**: isolate rerun-failures state file under tmpdir (Chemi Atlow) [#63449](https://github.com/nodejs/node/pull/63449) +* \[[`935468a49e`](https://github.com/nodejs/node/commit/935468a49e)] - **test**: fixup quic tests (James M Snell) [#63267](https://github.com/nodejs/node/pull/63267) +* \[[`fbbdfdcfc7`](https://github.com/nodejs/node/commit/fbbdfdcfc7)] - **test**: wait for ok before initial break after restart (Yuya Inoue) [#62807](https://github.com/nodejs/node/pull/62807) +* \[[`db808ad77d`](https://github.com/nodejs/node/commit/db808ad77d)] - **test**: unskip snapshot reproducibility test (Joyee Cheung) [#63307](https://github.com/nodejs/node/pull/63307) +* \[[`259d8b3dce`](https://github.com/nodejs/node/commit/259d8b3dce)] - **test**: update WPT for WebCryptoAPI to 97bbc7247a (Node.js GitHub Bot) [#63417](https://github.com/nodejs/node/pull/63417) +* \[[`d56c6cd708`](https://github.com/nodejs/node/commit/d56c6cd708)] - **test\_runner**: ignore erased TS lines in coverage (Matteo Collina) [#63510](https://github.com/nodejs/node/pull/63510) +* \[[`16015f1565`](https://github.com/nodejs/node/commit/16015f1565)] - **test\_runner**: fix suite diagnostic chanel end (Moshe Atlow) [#63533](https://github.com/nodejs/node/pull/63533) +* \[[`003b9ccbe9`](https://github.com/nodejs/node/commit/003b9ccbe9)] - **test\_runner**: dont buffer unordered events in process isolation mode (Moshe Atlow) [#63432](https://github.com/nodejs/node/pull/63432) +* \[[`fdc4b5aed4`](https://github.com/nodejs/node/commit/fdc4b5aed4)] - **test\_runner**: fix --test-rerun-failures swallowing failures on retry (Chemi Atlow) [#63431](https://github.com/nodejs/node/pull/63431) +* \[[`6a0bd2f329`](https://github.com/nodejs/node/commit/6a0bd2f329)] - **test\_runner**: add parentId to test events with testId (Moshe Atlow) [#63435](https://github.com/nodejs/node/pull/63435) +* \[[`a646c93254`](https://github.com/nodejs/node/commit/a646c93254)] - **test\_runner**: show replayed-from-attempt hint in spec reporter (Moshe Atlow) [#63429](https://github.com/nodejs/node/pull/63429) +* \[[`b1fa59cbb6`](https://github.com/nodejs/node/commit/b1fa59cbb6)] - **test\_runner**: preserve run duration when using test-rerun (Moshe Atlow) [#63429](https://github.com/nodejs/node/pull/63429) +* \[[`6ac7ff24ac`](https://github.com/nodejs/node/commit/6ac7ff24ac)] - **tools**: refine `v8.nix` source definition (Antoine du Hamel) [#63625](https://github.com/nodejs/node/pull/63625) +* \[[`59c01b959f`](https://github.com/nodejs/node/commit/59c01b959f)] - **tools**: add lint rule for aborted AbortController (Trivikram Kamat) [#63541](https://github.com/nodejs/node/pull/63541) +* \[[`2ab034f6f9`](https://github.com/nodejs/node/commit/2ab034f6f9)] - **tools**: bump @node-core/doc-kit in /tools/doc in the doc group (dependabot\[bot]) [#63494](https://github.com/nodejs/node/pull/63494) +* \[[`a6af903e0a`](https://github.com/nodejs/node/commit/a6af903e0a)] - **tools**: bump brace-expansion from 5.0.5 to 5.0.6 in /tools/eslint (dependabot\[bot]) [#63415](https://github.com/nodejs/node/pull/63415) +* \[[`215cd543dd`](https://github.com/nodejs/node/commit/215cd543dd)] - **tools**: skip commit-lint on backport pull requests (Marco) [#63378](https://github.com/nodejs/node/pull/63378) +* \[[`0479f28e95`](https://github.com/nodejs/node/commit/0479f28e95)] - **tools**: fix skip of `test-internet` on forks (Antoine du Hamel) [#63492](https://github.com/nodejs/node/pull/63492) +* \[[`69dfadf785`](https://github.com/nodejs/node/commit/69dfadf785)] - **tools**: mock some Python utils in `v8.nix` to reuse builds (Antoine du Hamel) [#63454](https://github.com/nodejs/node/pull/63454) +* \[[`7b3e222cda`](https://github.com/nodejs/node/commit/7b3e222cda)] - **util**: remove unused functions (Antoine du Hamel) [#63612](https://github.com/nodejs/node/pull/63612) +* \[[`5a1f67c27b`](https://github.com/nodejs/node/commit/5a1f67c27b)] - **util**: create hex style cache and fast path (Guilherme Araújo) [#62999](https://github.com/nodejs/node/pull/62999) + ## 2026-05-20, Version 26.2.0 (Current), @aduh95 diff --git a/lib/internal/errors.js b/lib/internal/errors.js index ba632359fbc185..cc8ad3db3540f1 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1321,6 +1321,8 @@ E('ERR_HTTP2_SOCKET_UNBOUND', E('ERR_HTTP2_STATUS_101', 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2', Error); E('ERR_HTTP2_STATUS_INVALID', 'Invalid status code: %s', RangeError); +E('ERR_HTTP2_STREAM_ABORTED', + 'The stream was reset before the readable side was fully consumed', Error); E('ERR_HTTP2_STREAM_CANCEL', function(error) { let msg = 'The pending stream has been canceled'; if (error) { diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index 6dd3b1e8be5c59..7b9524ef855988 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -158,11 +158,11 @@ function onStreamEnd() { } function onStreamError(error) { - // This is purposefully left blank - // - // errors in compatibility mode are - // not forwarded to the request - // and response objects. + // Mirror HTTP/1's IncomingMessage._destroy: forward 'error' to req + // only when a listener is attached. + const request = this[kRequest]; + if (request !== undefined && request.listenerCount('error') > 0) + request.emit('error', error); } function onRequestPause() { @@ -460,7 +460,9 @@ function onStreamCloseResponse() { this.removeListener('wantTrailers', onStreamTrailersReady); this[kResponse] = undefined; - res.emit('finish'); + // Only emit 'finish' when the underlying writable actually finished + if (this.writableFinished) + res.emit('finish'); res.emit('close'); } diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 1c6edd65cae8f0..7c009d60b95821 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -98,6 +98,7 @@ const { ERR_HTTP2_SOCKET_UNBOUND, ERR_HTTP2_STATUS_101, ERR_HTTP2_STATUS_INVALID, + ERR_HTTP2_STREAM_ABORTED, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_STREAM_ERROR, ERR_HTTP2_STREAM_SELF_DEPENDENCY, @@ -265,6 +266,8 @@ const kRequestAsyncResource = Symbol('requestAsyncResource'); const kSentHeaders = Symbol('sent-headers'); const kRawHeaders = Symbol('raw-headers'); const kSentTrailers = Symbol('sent-trailers'); +const kFilePipeFinalCb = Symbol('kFilePipeFinalCb'); +const kFilePipeReachedEOF = Symbol('kFilePipeReachedEOF'); const kServer = Symbol('server'); const kState = Symbol('state'); const kType = Symbol('type'); @@ -453,10 +456,17 @@ function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) { } debugStream(id, type, "emitting stream '%s' event", event); const reqAsync = stream[kRequestAsyncResource]; + // Mirrors HTTP/1's `if (!req.emit('response', res)) res._dump();`: + // drain the readable when the response has no listener. + const dispatch = () => { + const hadListeners = stream.emit(event, obj, flags, headers); + if (event === 'response' && !hadListeners) + autoDrainReadable.call(stream); + }; if (reqAsync) - reqAsync.runInAsyncScope(process.nextTick, null, emit, stream, event, obj, flags, headers); + reqAsync.runInAsyncScope(process.nextTick, null, dispatch); else - process.nextTick(emit, stream, event, obj, flags, headers); + process.nextTick(dispatch); if ((event === 'response' || event === 'push') && onClientStreamFinishChannel.hasSubscribers) { @@ -583,46 +593,90 @@ function onPing(payload) { session.emit('ping', payload); } -// Called when the stream is closed either by sending or receiving an -// RST_STREAM frame, or through a natural end-of-stream. -// If the writable and readable sides of the stream are still open at this -// point, close them. If there is an open fd for file send, close that also. -// At this point the underlying node::http2:Http2Stream handle is no -// longer usable so destroy it also. -function onStreamClose(code) { +// Fired by C++ when nghttp2's on_stream_close fires. `peerReset` is +// true when the peer sent a RST_STREAM frame - peer RST_STREAM(NO_ERROR) +// is otherwise indistinguishable from a clean END_STREAM exchange at +// this layer. +// +// 'error' fires iff the reset was peer-initiated AND either: +// * peer didn't END_STREAM (the consumer will never see 'end'), or +// * peer signalled a non-clean code while writes were still in flight. +// Clean code = NO_ERROR or CANCEL. +// +// Natural close defers destroy until 'end' and 'finish' - nghttp2 can +// fire on_stream_close ahead of our last frame leaving the socket. +function onStreamClose(code, peerReset) { const stream = this[kOwner]; if (!stream || stream.destroyed) return false; debugStreamObj( - stream, 'closed with code %d, closed %s, readable %s', - code, stream.closed, stream.readable, + stream, 'closed code=%d peerReset=%s closed=%s', + code, peerReset, stream.closed, ); - if (!stream.closed) + // stream.closed is set by closeStream; true here means we initiated. + const locallyInitiated = stream.closed; + if (!locallyInitiated) closeStream(stream, code, kNoRstStream); - stream[kState].fd = -1; - // Defer destroy we actually emit end. - if (!stream.readable || code !== NGHTTP2_NO_ERROR) { - // If errored or ended, we can destroy immediately. + + if (locallyInitiated) { stream.destroy(); - } else { - // Wait for end to destroy. - stream.on('end', stream[kMaybeDestroy]); - // Push a null so the stream can end whenever the client consumes - // it completely. - stream.push(null); + return true; + } - // If the user hasn't tried to consume the stream (and this is a server - // session) then just dump the incoming data so that the stream can - // be destroyed. - if (stream[kSession][kType] === NGHTTP2_SESSION_SERVER && - !stream[kState].didRead && - stream.readableFlowing === null) - stream.resume(); - else - stream.read(0); + const peerReadableEnded = stream._readableState.ended; + const cleanCode = code === NGHTTP2_NO_ERROR || code === NGHTTP2_CANCEL; + + if (!peerReadableEnded) { + stream.destroy(cleanCode ? + new ERR_HTTP2_STREAM_ABORTED() : + new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code)); + return true; + } + + // Peer END_STREAMed - reads will complete. Only the writable half can + // still be in flight at the protocol layer. + if (peerReset) { + if (!cleanCode && !stream._writableState.finished) { + stream.destroy( + new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code)); + } else { + stream.destroy(); + } + return true; + } + + // Natural close: wait for 'end' and 'finish'. autoDestroy is disabled + // on Http2Stream, so an errored writable won't fire 'finish' and an + // errored readable won't fire 'end' - and on a Duplex a writable + // error propagates to readable.errored, blocking 'end' too. Treat + // either side's errored state as settled. + const readDone = () => stream._readableState.endEmitted || + !!stream._readableState.errored; + const writeDone = () => stream._writableState.finished || + !!stream._writableState.errored; + if (readDone() && writeDone()) { + stream.destroy(); + return true; + } + + const maybeDestroy = () => { + if (!stream.destroyed && readDone() && writeDone()) + stream.destroy(); + }; + stream.once('end', maybeDestroy); + stream.once('finish', maybeDestroy); + stream.once('error', maybeDestroy); + if (stream[kSession][kType] === NGHTTP2_SESSION_SERVER && + !stream[kState].didRead && + stream.readableFlowing === null) { + // On the server, if you don't try to read the request body + // at all before the stream closes, we dump it, like HTTP/1. + stream.resume(); + } else { + stream.read(0); } return true; } @@ -2006,37 +2060,47 @@ const kNoRstStream = 0; const kSubmitRstStream = 1; const kForceRstStream = 2; +// HTTP/1-parity auto-drain: silently drain unread bytes off the wire +// so the stream can close instead of zombieing. Used as a 'finish' +// listener on server streams (mirrors req._dump from resOnFinish) and +// called directly when a client stream's 'response' event has no +// listener (mirrors res._dump from parserOnIncomingClient). +function autoDrainReadable() { + if (!this[kState].didRead && + this.readableFlowing === null && + !this.destroyed) { + this.resume(); + } +} + function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { const type = stream[kSession][kType]; const state = stream[kState]; state.flags |= STREAM_FLAGS_CLOSED; state.rstCode = code; - // Clear timeout and remove timeout listeners stream.setTimeout(0); stream.removeAllListeners('timeout'); + // Emit 'aborted' if the user hadn't ended the writable yet. This is + // unusual and doesn't match 'error' but is deprecated & preserved for + // backward compat (DEP0207). const { ending } = stream._writableState; - - if (!ending) { - // If the writable side of the Http2Stream is still open, emit the - // 'aborted' event and set the aborted flag. - if (!stream.aborted) { - state.flags |= STREAM_FLAGS_ABORTED; - stream.emit('aborted'); - } - - // Close the writable side. - stream.end(); + if (!ending && !stream.aborted) { + state.flags |= STREAM_FLAGS_ABORTED; + stream.emit('aborted'); } if (rstStreamStatus !== kNoRstStream) { - const finishFn = finishCloseStream.bind(stream, code); + // If the user already called .end() and writes are still draining, + // wait for 'finish' so the in-flight bytes land before sending + // RST(NO_ERROR). Anything else submits immediately. if (!ending || stream.writableFinished || code !== NGHTTP2_NO_ERROR || - rstStreamStatus === kForceRstStream) - finishFn(); - else - stream.once('finish', finishFn); + rstStreamStatus === kForceRstStream) { + finishCloseStream.call(stream, code); + } else { + stream.once('finish', finishCloseStream.bind(stream, code)); + } } if (type === NGHTTP2_SESSION_CLIENT) { @@ -2050,11 +2114,8 @@ function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { function finishCloseStream(code) { const rstStreamFn = submitRstStream.bind(this, code); - // If the handle has not yet been assigned, queue up the request to - // ensure that the RST_STREAM frame is sent after the stream ID has - // been determined. + // Defer until the stream has an ID assigned. if (this.pending) { - this.push(null); this.once('ready', rstStreamFn); return; } @@ -2469,6 +2530,7 @@ class Http2Stream extends Duplex { closeStream(this, code, hasHandle ? kForceRstStream : kNoRstStream); this.push(null); + if (hasHandle) { handle.destroy(); sessionState.streams.delete(id); @@ -2480,9 +2542,8 @@ class Http2Stream extends Duplex { sessionState.writeQueueSize -= state.writeQueueSize; state.writeQueueSize = 0; - // RST code 8 not emitted as an error as its used by clients to signify - // abort and is already covered by aborted event, also allows more - // seamless compatibility with http1 + // Synthesize an error for non-zero RST codes if the caller didn't + // pass one (e.g. direct stream.destroy() following a peer error). if (err == null && code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL) err = new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code); @@ -2688,13 +2749,23 @@ function onFileUnpipe() { this.source.close().catch(stream.destroy.bind(stream)); else this.source.releaseFD(); + + // Resolve the deferred _final callback only if we actually + // reached the EOF. Otherwise we close with no finish. + const cb = stream[kFilePipeFinalCb]; + if (cb && stream[kFilePipeReachedEOF]) { + stream[kFilePipeFinalCb] = undefined; + cb(); + } } // This is only called once the pipe has returned back control, so // it only has to handle errors and End-of-File. function onPipedFileHandleRead() { const err = streamBaseState[kReadBytesOrError]; - if (err < 0 && err !== UV_EOF) { + if (err === UV_EOF) { + this.stream[kFilePipeReachedEOF] = true; + } else if (err < 0) { this.stream.close(NGHTTP2_INTERNAL_ERROR); } } @@ -2719,9 +2790,12 @@ function processRespondWithFD(self, fd, headers, offset = 0, length = -1, } self[kSentHeaders] = headers; - // Close the writable side of the stream, but only as far as the writable - // stream implementation is concerned. - self._final = null; + // To make 'finish'/writableFinished work correctly with the C++ file + // pipe, we defer the callback here, and call it from onFileUnpipe. + self._final = (cb) => { + self[kFilePipeFinalCb] = cb; + }; + self[kFilePipeReachedEOF] = false; self.end(); const ret = self[kHandle].respond(headersList, streamOptions); @@ -2885,6 +2959,7 @@ class ServerHttp2Stream extends Http2Stream { this[kInit](id, handle); this[kProtocol] = headers[HTTP2_HEADER_SCHEME]; this[kAuthority] = getAuthority(headers); + this.once('finish', autoDrainReadable); } // True if the remote peer accepts push streams diff --git a/src/node_http2.cc b/src/node_http2.cc index 55bca557a81e4d..f90fb612db681f 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -1101,6 +1101,16 @@ int Http2Session::OnFrameReceive(nghttp2_session* handle, case NGHTTP2_HEADERS: session->HandleHeadersFrame(frame); break; + case NGHTTP2_RST_STREAM: + // Stamp the stream so JS onStreamClose can tell a peer reset apart + // from a clean close — both surface as on_stream_close, and a peer + // RST_STREAM(NO_ERROR) is otherwise indistinguishable from a natural + // close at the on_stream_close layer. + if (BaseObjectPtr stream = + session->FindStream(frame->hd.stream_id)) { + stream->set_peer_reset(); + } + break; case NGHTTP2_SETTINGS: session->HandleSettingsFrame(frame); break; @@ -1310,9 +1320,12 @@ int Http2Session::OnStreamClose(nghttp2_session* handle, // ever passed on to the javascript side. If that happens, the callback // will return false. if (env->can_call_into_js()) { - Local arg = Integer::NewFromUnsigned(isolate, code); + Local argv[2] = { + Integer::NewFromUnsigned(isolate, code), + Boolean::New(isolate, stream->peer_reset()), + }; MaybeLocal answer = stream->MakeCallback( - env->http2session_on_stream_close_function(), 1, &arg); + env->http2session_on_stream_close_function(), arraysize(argv), argv); if (answer.IsEmpty() || answer.ToLocalChecked()->IsFalse()) { // Skip to destroy stream->Destroy(); diff --git a/src/node_http2.h b/src/node_http2.h index 28245d7b98e06b..7ebae09a4b333e 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -65,6 +65,7 @@ constexpr int kStreamStateReadPaused = 0x4; constexpr int kStreamStateClosed = 0x8; constexpr int kStreamStateDestroyed = 0x10; constexpr int kStreamStateTrailers = 0x20; +constexpr int kStreamStatePeerReset = 0x40; // Http2Session internal states constexpr int kSessionStateNone = 0x0; @@ -347,6 +348,15 @@ class Http2Stream : public AsyncWrap, return flags_ & kStreamStateClosed; } + // True iff a RST_STREAM frame was received from the peer for this stream. + // Set by Http2Session::OnFrameReceive on NGHTTP2_RST_STREAM. Used by JS + // onStreamClose to distinguish a peer-initiated reset from a clean + // bidirectional END_STREAM exchange (both surface to JS with the same + // nghttp2 close code when the peer sent RST_STREAM(NO_ERROR)). + bool peer_reset() const { return flags_ & kStreamStatePeerReset; } + + void set_peer_reset() { flags_ |= kStreamStatePeerReset; } + bool has_trailers() const { return flags_ & kStreamStateTrailers; } diff --git a/test/parallel/test-http2-client-auto-discard-no-response-listener.js b/test/parallel/test-http2-client-auto-discard-no-response-listener.js new file mode 100644 index 00000000000000..e9f414dfb1f786 --- /dev/null +++ b/test/parallel/test-http2-client-auto-discard-no-response-listener.js @@ -0,0 +1,49 @@ +'use strict'; + +// HTTP/1 parity for fire-and-forget clients: when no 'response' +// listener is attached at the time response headers arrive, the +// response body is silently drained (mirrors res._dump from +// parserOnIncomingClient). After that drains, 'end' fires and the +// stream closes cleanly — no 'aborted', no 'error'. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + // Send a non-trivial response body so the client's readable buffer + // would actually contain something to discard. + stream.respond({ ':status': 200 }); + stream.end(Buffer.alloc(16 * 1024)); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const cs = client.request({ ':method': 'GET' }); + + // Deliberately do NOT attach a 'response' listener — that's the + // condition that triggers H1's `_dump` and our parallel auto-discard. + // Adding a listener here (even a `common.mustNotCall`) would make + // listenerCount > 0 and skip the auto-discard. + + cs.on('aborted', common.mustNotCall( + "'aborted' must not fire for fire-and-forget clients")); + cs.on('error', common.mustNotCall( + "'error' must not fire for fire-and-forget clients")); + + // 'end' must fire once auto-discard resumes the readable. + cs.on('end', common.mustCall()); + + cs.on('close', common.mustCall(() => { + assert.strictEqual(cs.aborted, false); + assert.strictEqual(cs.destroyed, true); + client.close(); + server.close(); + })); + + cs.end(); +})); diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js index c0b59551d48232..90b2e5be93c773 100644 --- a/test/parallel/test-http2-client-rststream-before-connect.js +++ b/test/parallel/test-http2-client-rststream-before-connect.js @@ -71,9 +71,9 @@ server.listen(0, common.mustCall(() => { // RST_STREAM frame before it ever has a chance to reply. req.on('response', common.mustNotCall()); - // The `end` event should still fire as we close the readable stream by - // pushing a `null` chunk. - req.on('end', common.mustCall()); + // Any non-clean local close triggers an 'error', and the readable's + // errored state blocks 'end' - matching HTTP/1 ECONNRESET behaviour. + req.on('end', common.mustNotCall()); req.resume(); req.end(); diff --git a/test/parallel/test-http2-close-while-writing.js b/test/parallel/test-http2-close-while-writing.js index c0a05c4a8da21a..17931005dc6cd9 100644 --- a/test/parallel/test-http2-close-while-writing.js +++ b/test/parallel/test-http2-close-while-writing.js @@ -1,6 +1,7 @@ 'use strict'; // https://github.com/nodejs/node/issues/33156 const common = require('../common'); +const assert = require('assert'); const fixtures = require('../common/fixtures'); if (!common.hasCrypto) { @@ -23,6 +24,11 @@ let client_stream; server.on('session', common.mustCall(function(session) { session.on('stream', common.mustCall(function(stream) { + // Client destroys mid-stream without END_STREAM (clean RST code). + // Peer reset before END_STREAM surfaces as ERR_HTTP2_STREAM_ABORTED. + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ABORTED'); + })); stream.resume(); stream.on('data', function() { this.write(Buffer.alloc(1)); diff --git a/test/parallel/test-http2-compat-errors.js b/test/parallel/test-http2-compat-errors.js index 18dc385422a48e..0b37eaed8d00cd 100644 --- a/test/parallel/test-http2-compat-errors.js +++ b/test/parallel/test-http2-compat-errors.js @@ -3,16 +3,20 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const assert = require('assert'); const h2 = require('http2'); -// Errors should not be reported both in Http2ServerRequest -// and Http2ServerResponse +// Errors on the underlying stream surface on Http2ServerRequest let expected = null; const server = h2.createServer(common.mustCall(function(req, res) { - res.stream.on('error', common.mustCall()); - req.on('error', common.mustNotCall()); + res.stream.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); + req.on('error', common.mustCall((err) => { + assert.strictEqual(err, expected); + })); res.on('error', common.mustNotCall()); req.on('aborted', common.mustCall()); res.on('aborted', common.mustNotCall()); diff --git a/test/parallel/test-http2-compat-serverresponse-destroy.js b/test/parallel/test-http2-compat-serverresponse-destroy.js index 1154b69df41548..40b0a819eb8637 100644 --- a/test/parallel/test-http2-compat-serverresponse-destroy.js +++ b/test/parallel/test-http2-compat-serverresponse-destroy.js @@ -16,10 +16,13 @@ const errors = [ let nextError; const server = http2.createServer(common.mustCall((req, res) => { - req.on('error', common.mustNotCall()); + if (req.url !== '/') + req.on('error', common.mustCall()); + else + req.on('error', common.mustNotCall()); res.on('error', common.mustNotCall()); - res.on('finish', common.mustCall(() => { + res.on('close', common.mustCall(() => { res.destroy(nextError); process.nextTick(() => { res.destroy(nextError); @@ -44,8 +47,14 @@ server.listen(0, common.mustCall(() => { { const req = client.request(); req.on('response', common.mustNotCall()); - req.on('error', common.mustNotCall()); - req.on('end', common.mustCall()); + // Peer sends RST(NO_ERROR) before END_STREAM — surfaces as + // ERR_HTTP2_STREAM_ABORTED, matching HTTP/1's ECONNRESET on a + // peer-side socket.destroy(). + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ABORTED', + name: 'Error', + })); + req.on('end', common.mustNotCall()); req.on('close', common.mustCall(() => countdown.dec())); req.resume(); } diff --git a/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js b/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js index fc97a70f42d956..81e1977a788c7b 100644 --- a/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js +++ b/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js @@ -13,7 +13,7 @@ const server = h2.createServer(); server.listen(0, common.mustCall(function() { const port = server.address().port; server.once('request', common.mustCall(function(request, response) { - response.on('finish', common.mustCall(() => { + response.on('close', common.mustCall(() => { assert.strictEqual(response.headersSent, false); response.setHeader('test', 'value'); response.removeHeader('test', 'value'); @@ -38,7 +38,8 @@ server.listen(0, common.mustCall(function() { ':authority': `localhost:${port}` }; const request = client.request(headers); - request.on('end', common.mustCall(function() { + request.on('error', () => {}); + request.on('close', common.mustCall(function() { client.close(); })); request.end(); diff --git a/test/parallel/test-http2-compat-socket-destroy-delayed.js b/test/parallel/test-http2-compat-socket-destroy-delayed.js index 62405047d8266e..0485604d63c7af 100644 --- a/test/parallel/test-http2-compat-socket-destroy-delayed.js +++ b/test/parallel/test-http2-compat-socket-destroy-delayed.js @@ -7,7 +7,6 @@ if (!common.hasCrypto) common.skip('missing crypto'); const http2 = require('http2'); -const assert = require('assert'); const { HTTP2_HEADER_PATH, @@ -30,10 +29,13 @@ app.listen(0, mustCall(() => { [HTTP2_HEADER_METHOD]: 'get' }); request.once('response', mustCall((headers, flags) => { - let data = ''; - request.on('data', (chunk) => { data += chunk; }); - request.on('end', mustCall(() => { - assert.strictEqual(data, 'hello'); + request.on('data', () => {}); + // setImmediate(socket.destroy) after res.end races with the response + // bytes reaching the client; if they don't make it the reset surfaces + // as 'error'. Test only checks that destroy-via-setImmediate doesn't + // crash, so accept either path. + request.on('error', () => {}); + request.on('close', mustCall(() => { session.close(); app.close(); })); diff --git a/test/parallel/test-http2-compat-socket-set.js b/test/parallel/test-http2-compat-socket-set.js index e8b804858d65bd..8612e63bff5ce3 100644 --- a/test/parallel/test-http2-compat-socket-set.js +++ b/test/parallel/test-http2-compat-socket-set.js @@ -87,7 +87,8 @@ server.listen(0, common.mustCall(function() { ':authority': `localhost:${port}` }; const request = client.request(headers); - request.on('end', common.mustCall(() => { + request.on('error', () => {}); + request.on('close', common.mustCall(() => { client.close(); server.close(); })); diff --git a/test/parallel/test-http2-compat-socket.js b/test/parallel/test-http2-compat-socket.js index 95bc42180ef8e9..392e9205eb386c 100644 --- a/test/parallel/test-http2-compat-socket.js +++ b/test/parallel/test-http2-compat-socket.js @@ -51,7 +51,7 @@ server.on('request', common.mustCall(function(request, response) { assert.strictEqual(request.socket.readable, false); response.socket.destroy(); })); - response.on('finish', common.mustCall(() => { + response.on('close', common.mustCall(() => { assert.ok(request.socket); assert.strictEqual(response.socket, undefined); assert.ok(request.socket.destroyed); @@ -84,7 +84,8 @@ server.listen(0, common.mustCall(function() { ':authority': `localhost:${port}` }; const request = client.request(headers); - request.on('end', common.mustCall(() => { + request.on('error', () => {}); + request.on('close', common.mustCall(() => { client.close(); })); request.end(); diff --git a/test/parallel/test-http2-compat-write-head-after-close.js b/test/parallel/test-http2-compat-write-head-after-close.js index 541973f5dbc5c6..2d2651b370f919 100644 --- a/test/parallel/test-http2-compat-write-head-after-close.js +++ b/test/parallel/test-http2-compat-write-head-after-close.js @@ -15,7 +15,8 @@ server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${port}`); const req = client.request({ ':path': '/' }); req.on('response', common.mustNotCall('head after close should not be sent')); - req.on('end', common.mustCall(() => { + req.on('error', () => {}); + req.on('close', common.mustCall(() => { client.close(); server.close(); })); diff --git a/test/parallel/test-http2-compat-write-head-destroyed.js b/test/parallel/test-http2-compat-write-head-destroyed.js index 842bf0e9abffb0..9ab1228c243779 100644 --- a/test/parallel/test-http2-compat-write-head-destroyed.js +++ b/test/parallel/test-http2-compat-write-head-destroyed.js @@ -21,6 +21,7 @@ server.listen(0, common.mustCall(() => { const req = client.request(); req.on('response', common.mustNotCall()); + req.on('error', () => {}); req.on('close', common.mustCall((arg) => { client.close(); server.close(); diff --git a/test/parallel/test-http2-destroy-after-write.js b/test/parallel/test-http2-destroy-after-write.js index 40fae332c18baa..73ee3ec3c0d23f 100644 --- a/test/parallel/test-http2-destroy-after-write.js +++ b/test/parallel/test-http2-destroy-after-write.js @@ -28,6 +28,7 @@ server.listen(0, common.mustCall(() => { stream.on('response', common.mustCall(function(headers) { assert.strictEqual(headers[':status'], 200); })); + stream.on('error', () => {}); stream.on('close', common.mustCall(() => { client.close(); server.close(); diff --git a/test/parallel/test-http2-large-write-destroy.js b/test/parallel/test-http2-large-write-destroy.js index b59c66bb04755b..e4fe6c6d49e80e 100644 --- a/test/parallel/test-http2-large-write-destroy.js +++ b/test/parallel/test-http2-large-write-destroy.js @@ -34,6 +34,7 @@ server.listen(0, common.mustCall(() => { req.end(); req.resume(); // Otherwise close won't be emitted if there's pending data. + req.on('error', () => {}); req.on('close', common.mustCall(() => { client.close(); server.close(); diff --git a/test/parallel/test-http2-many-writes-and-destroy.js b/test/parallel/test-http2-many-writes-and-destroy.js index 51ec119b6295c3..3c3256073da1a1 100644 --- a/test/parallel/test-http2-many-writes-and-destroy.js +++ b/test/parallel/test-http2-many-writes-and-destroy.js @@ -7,6 +7,9 @@ const http2 = require('http2'); { const server = http2.createServer((req, res) => { + // Peer destroys mid-stream, so we see errors here: + req.on('error', () => {}); + res.on('error', () => {}); req.pipe(res); }); diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js index ff4ae541423dce..21627f1e23a901 100644 --- a/test/parallel/test-http2-misused-pseudoheaders.js +++ b/test/parallel/test-http2-misused-pseudoheaders.js @@ -40,9 +40,12 @@ server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); const req = client.request(); + // Server uses waitForTrailers, fails to send valid trailers, then + // close()s — peer reset arrives before END_STREAM, surfacing as + // 'error' on the client. req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall()); + req.on('error', () => {}); req.on('close', common.mustCall(() => { server.close(); client.close(); diff --git a/test/parallel/test-http2-reset-aborts-readable-not-drained.js b/test/parallel/test-http2-reset-aborts-readable-not-drained.js new file mode 100644 index 00000000000000..b436e96a22e666 --- /dev/null +++ b/test/parallel/test-http2-reset-aborts-readable-not-drained.js @@ -0,0 +1,64 @@ +'use strict'; + +// When the peer sends RST_STREAM without first END_STREAMing, the +// consumer will never see 'end' - surface that as 'aborted' + +// 'error' + 'close' (no 'end', no 'finish'). + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const { NGHTTP2_NO_ERROR } = http2.constants; + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + let abortedFiredAt = -1; + let errorFiredAt = -1; + let closeFiredAt = -1; + let tick = 0; + + stream.on('aborted', common.mustCall(() => { + abortedFiredAt = ++tick; + // 'aborted' fires while the stream is still alive but flagged. + assert.strictEqual(stream.aborted, true); + })); + + stream.on('error', common.mustCall((err) => { + errorFiredAt = ++tick; + assert.ok(err && typeof err.code === 'string' && + err.code.startsWith('ERR_HTTP2_'), + `expected an ERR_HTTP2_* error, got ${err?.code}`); + })); + + stream.on('end', common.mustNotCall( + "'end' must not fire - readable was not drained at reset")); + stream.on('finish', common.mustNotCall( + "'finish' must not fire - stream was reset, not ended")); + + stream.on('close', common.mustCall(() => { + closeFiredAt = ++tick; + assert.strictEqual(stream.aborted, true); + assert.strictEqual(stream.destroyed, true); + assert.ok(abortedFiredAt !== -1, "'aborted' must fire before 'close'"); + assert.ok(errorFiredAt !== -1, "'error' must fire before 'close'"); + assert.ok(abortedFiredAt < closeFiredAt && + errorFiredAt < closeFiredAt, + `'close' must come last (aborted=${abortedFiredAt} ` + + `error=${errorFiredAt} close=${closeFiredAt})`); + server.close(); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const cs = client.request({ ':method': 'POST' }); + cs.on('close', common.mustCall(() => client.close())); + + cs.write('payload that will never be consumed'); + // Send RST_STREAM. Note we deliberately do NOT call cs.end() - the + // server will see no END_STREAM, only the reset. + cs.close(NGHTTP2_NO_ERROR); +})); diff --git a/test/parallel/test-http2-reset-happy-close.js b/test/parallel/test-http2-reset-happy-close.js new file mode 100644 index 00000000000000..d4778de8c81d97 --- /dev/null +++ b/test/parallel/test-http2-reset-happy-close.js @@ -0,0 +1,120 @@ +'use strict'; + +// A peer RST_STREAM that arrives AFTER our readable has fully drained +// AND while writes are still in flight: 'finish' must not fire (writes +// were aborted) but every pending _write callback eventually fires +// (success for writes that made it onto the wire, ECANCELED for those +// still queued at reset time). 'aborted' fires per the legacy criterion +// since the writable was still open at close - see DEP0207. +// +// Whether 'error' fires depends on the peer's reset code: +// - Clean codes (NO_ERROR/CANCEL): no 'error' — the cancel is treated +// as a clean cancellation, not a failure. +// - Other codes: 'error' fires with ERR_HTTP2_STREAM_ERROR. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const { + NGHTTP2_NO_ERROR, + NGHTTP2_INTERNAL_ERROR, +} = http2.constants; + +const N_WRITES = 16; +const CHUNK_BYTES = 8192; +// 16 * 8 KiB = 128 KiB, larger than the default 64 KiB initial flow- +// control window - so even if the OS socket buffer accepts everything +// instantly, the server's nghttp2 will hold some chunks in its outbound +// queue (waiting on a WINDOW_UPDATE that the client will never send). +// That guarantees we exercise the stranded-writes path even when the +// client RSTs after consuming HEADERS. + +function runScenario(rstCode, expectedErrorCode, next) { + const server = http2.createServer(); + + server.on('stream', common.mustCall((stream) => { + // Drain the request body fully. + stream.resume(); + stream.on('end', common.mustCall(() => { + // Readable is drained. Now begin streaming a response that is too + // large to land before the client RSTs. + stream.respond({ ':status': 200 }); + + let writeCbCount = 0; + for (let i = 0; i < N_WRITES; i++) { + stream.write(Buffer.alloc(CHUNK_BYTES), () => { writeCbCount++; }); + } + + stream.on('aborted', common.mustCall(() => { + assert.strictEqual(stream.aborted, true); + })); + stream.on('finish', common.mustNotCall( + "'finish' must not fire when writable was aborted by reset")); + + if (expectedErrorCode === null) { + stream.on('error', common.mustNotCall( + "'error' must not fire for a clean-code peer reset")); + } else { + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, expectedErrorCode); + })); + } + + stream.on('close', common.mustCall(() => { + // 'aborted' fires per the legacy criterion (writable was still + // open at reset). + assert.strictEqual(stream.aborted, true); + // 'finish' must not have been emitted (writes were aborted). + assert.strictEqual(stream.writableFinished, false); + assert.strictEqual(stream.destroyed, true); + // The C++ Http2Stream::Destroy SetImmediate flushes any + // still-queued WriteWraps with UV_ECANCELED, which fires the + // remaining `_write` callbacks. Those land *after* 'close'; + // verify the full set has fired by the next two immediates. + setImmediate(common.mustCall(() => setImmediate(common.mustCall(() => { + assert.strictEqual(writeCbCount, N_WRITES, + `all ${N_WRITES} write callbacks must fire ` + + `(got ${writeCbCount})`); + server.close(next); + })))); + })); + })); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const cs = client.request({ ':method': 'POST' }); + cs.on('close', common.mustCall(() => client.close())); + + // Local close() with a non-clean code synthesizes 'error' in + // _destroy; clean codes are silent. Assert exactly the expected + // shape on the client side too. + if (expectedErrorCode === null) { + cs.on('error', common.mustNotCall( + "'error' must not fire on locally-initiated close with clean code")); + } else { + cs.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, expectedErrorCode); + })); + } + + // Tiny request body; end immediately so the server's readable drains + // cleanly before we reset. + cs.end(Buffer.alloc(8)); + + cs.on('response', common.mustCall(() => { + // We've seen the response HEADERS; the response body may or may not + // have started arriving. RST without consuming any of it. + cs.close(rstCode); + })); + })); +} + +// Clean code: no 'error'. +runScenario(NGHTTP2_NO_ERROR, null, () => { + // Non-clean code: 'error' fires with ERR_HTTP2_STREAM_ERROR. + runScenario(NGHTTP2_INTERNAL_ERROR, 'ERR_HTTP2_STREAM_ERROR', () => {}); +}); diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js index cc733b6994a3bd..c578a619b84758 100644 --- a/test/parallel/test-http2-respond-errors.js +++ b/test/parallel/test-http2-respond-errors.js @@ -43,7 +43,8 @@ server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); - req.on('end', common.mustCall(() => { + req.on('error', () => {}); + req.on('close', common.mustCall(() => { client.close(); server.close(); })); diff --git a/test/parallel/test-http2-respond-file-errors.js b/test/parallel/test-http2-respond-file-errors.js index 5c3424f2bc3484..46d29273ae0317 100644 --- a/test/parallel/test-http2-respond-file-errors.js +++ b/test/parallel/test-http2-respond-file-errors.js @@ -94,6 +94,7 @@ server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); + req.on('error', () => {}); req.on('close', common.mustCall(() => { client.close(); server.close(); diff --git a/test/parallel/test-http2-respond-file-fd-errors.js b/test/parallel/test-http2-respond-file-fd-errors.js index 5f7e57ea4e45d2..27838f3f3c2cda 100644 --- a/test/parallel/test-http2-respond-file-fd-errors.js +++ b/test/parallel/test-http2-respond-file-fd-errors.js @@ -117,6 +117,7 @@ server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); + req.on('error', () => {}); req.on('close', common.mustCall(() => { client.close(); server.close(); diff --git a/test/parallel/test-http2-respond-with-fd-finish.js b/test/parallel/test-http2-respond-with-fd-finish.js new file mode 100644 index 00000000000000..c93858b9aac0b1 --- /dev/null +++ b/test/parallel/test-http2-respond-with-fd-finish.js @@ -0,0 +1,100 @@ +'use strict'; + +// respondWithFD's 'finish' event must reflect actual file-pipe completion +// (matching every other Writable), not the moment respondWithFD was called. +// +// - Clean EOF: 'finish' fires, writableFinished=true. +// - Aborted before pipe completes: 'finish' does NOT fire, +// writableFinished stays false. ('aborted' is preserved per its +// legacy criterion: writable was 'ending' at close, so it doesn't +// fire either way.) + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const http2 = require('http2'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +function testCleanCompletion(next) { + const smallPath = path.join(tmpdir.path, 'small.bin'); + const smallSize = 64; + fs.writeFileSync(smallPath, Buffer.alloc(smallSize)); + + const server = http2.createServer(); + server.on('stream', common.mustCall((stream) => { + stream.on('finish', common.mustCall(() => { + assert.strictEqual(stream.writableFinished, true); + })); + stream.on('error', common.mustNotCall()); + stream.on('close', common.mustCall(() => { + assert.strictEqual(stream.writableFinished, true); + })); + const fd = fs.openSync(smallPath, 'r'); + stream.ownsFd = true; + stream.respondWithFD(fd, { 'content-length': smallSize }); + })); + + // Compat 'finish' fires only on real completion too. + server.on('request', common.mustCall((req, res) => { + res.on('finish', common.mustCall()); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.resume(); + req.on('end', common.mustCall()); + req.on('close', common.mustCall(() => { + client.close(); + server.close(next); + })); + })); +} + +function testAbortBeforeCompletion() { + const smallPath = path.join(tmpdir.path, 'small-abort.bin'); + const smallSize = 64; + fs.writeFileSync(smallPath, Buffer.alloc(smallSize)); + + const server = http2.createServer(); + server.on('stream', common.mustCall((stream) => { + stream.on('finish', common.mustNotCall( + "'finish' must not fire on aborted file pipe")); + stream.on('aborted', common.mustNotCall( + "'aborted' must not fire - preserving deprecated legacy behaviour")); + stream.on('error', () => {}); + stream.on('close', common.mustCall(() => { + // writableFinished must reflect actual pipe completion. + assert.strictEqual(stream.writableFinished, false); + })); + const fd = fs.openSync(smallPath, 'r'); + stream.ownsFd = true; + stream.respondWithFD(fd, { 'content-length': smallSize }); + // Synchronously interrupt the pipe before it can read EOF. + stream.destroy(); + })); + + // Compat 'finish' must not fire on aborted pipe either. + server.on('request', common.mustCall((req, res) => { + res.on('finish', common.mustNotCall( + "compat res 'finish' must not fire on aborted file pipe")); + res.on('close', common.mustCall()); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('error', () => {}); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); + })); +} + +testCleanCompletion(testAbortBeforeCompletion); diff --git a/test/parallel/test-http2-server-respond-without-reading-body.js b/test/parallel/test-http2-server-respond-without-reading-body.js new file mode 100644 index 00000000000000..750d83cf2c600b --- /dev/null +++ b/test/parallel/test-http2-server-respond-without-reading-body.js @@ -0,0 +1,51 @@ +'use strict'; + +// Server-side auto-drain (mirrors HTTP/1's req._dump): when a server +// finishes its response without ever consuming the request body, the +// body is silently drained on 'finish' so 'end' can fire and the +// stream closes cleanly - no 'aborted', no 'error'. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream, headers) => { + // Deliberately do NOT read the request body. Just respond and end. + // The server-side auto-drain should kick in on 'finish' and pull + // the body off the wire so 'end' can fire. + stream.respond({ ':status': 200 }); + stream.end('ok'); + + // 'end' on the readable must fire - the auto-drain doing its job. + stream.on('end', common.mustCall(() => { + assert.strictEqual(stream._readableState.endEmitted, true); + })); + + stream.on('aborted', common.mustNotCall( + "'aborted' must not fire when the auto-drain succeeded")); + stream.on('error', common.mustNotCall( + "'error' must not fire on a clean response without reading the body")); + + stream.on('close', common.mustCall(() => { + // stream.aborted must be false after auto-drain. + assert.strictEqual(stream.aborted, false); + assert.strictEqual(stream.destroyed, true); + server.close(); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + // Send a non-trivial request body so there's actual data buffered on + // the server side before the auto-drain runs. Without the drain, + // `endEmitted` would stay false until the buffer is consumed. + const cs = client.request({ ':method': 'POST' }); + cs.resume(); + cs.on('close', common.mustCall(() => client.close())); + cs.end(Buffer.alloc(16 * 1024)); +})); diff --git a/test/parallel/test-http2-server-rst-before-respond.js b/test/parallel/test-http2-server-rst-before-respond.js index d551c7121f5b7c..a29a20aae9428c 100644 --- a/test/parallel/test-http2-server-rst-before-respond.js +++ b/test/parallel/test-http2-server-rst-before-respond.js @@ -28,6 +28,9 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); const req = client.request(); req.on('headers', common.mustNotCall()); + req.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ABORTED'); + })); req.on('close', common.mustCall(() => { assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, req.rstCode); server.close(); diff --git a/test/parallel/test-http2-server-rst-stream.js b/test/parallel/test-http2-server-rst-stream.js index 9f37ce71472353..8243cfe2d98032 100644 --- a/test/parallel/test-http2-server-rst-stream.js +++ b/test/parallel/test-http2-server-rst-stream.js @@ -15,27 +15,33 @@ const { NGHTTP2_INTERNAL_ERROR } = http2.constants; +// Each entry: [rstcode, errorCode]. A peer-initiated RST that arrives +// before END_STREAM surfaces 'aborted' + 'error' — clean codes +// (NO_ERROR/CANCEL) yield ERR_HTTP2_STREAM_ABORTED, other codes yield +// ERR_HTTP2_STREAM_ERROR. const tests = [ - [NGHTTP2_NO_ERROR, false], - [NGHTTP2_NO_ERROR, false], - [NGHTTP2_PROTOCOL_ERROR, true, 'NGHTTP2_PROTOCOL_ERROR'], - [NGHTTP2_CANCEL, false], - [NGHTTP2_REFUSED_STREAM, true, 'NGHTTP2_REFUSED_STREAM'], - [NGHTTP2_INTERNAL_ERROR, true, 'NGHTTP2_INTERNAL_ERROR'], + [NGHTTP2_NO_ERROR, 'ERR_HTTP2_STREAM_ABORTED'], + [NGHTTP2_PROTOCOL_ERROR, 'ERR_HTTP2_STREAM_ERROR'], + [NGHTTP2_CANCEL, 'ERR_HTTP2_STREAM_ABORTED'], + [NGHTTP2_REFUSED_STREAM, 'ERR_HTTP2_STREAM_ERROR'], + [NGHTTP2_INTERNAL_ERROR, 'ERR_HTTP2_STREAM_ERROR'], ]; const server = http2.createServer(); -server.on('stream', (stream, headers) => { - const test = tests.find((t) => t[0] === Number(headers.rstcode)); - if (test[1]) { - stream.on('error', common.expectsError({ - name: 'Error', - code: 'ERR_HTTP2_STREAM_ERROR', - message: `Stream closed with error code ${test[2]}` +server.on('stream', common.mustCall((stream, headers) => { + const code = headers.rstcode | 0; + // Locally-initiated close synthesizes 'error' iff the code isn't clean + // (NO_ERROR/CANCEL). Assert exactly that. + if (code === NGHTTP2_NO_ERROR || code === NGHTTP2_CANCEL) { + stream.on('error', common.mustNotCall( + "'error' must not fire on locally-initiated close with a clean code")); + } else { + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR'); })); } - stream.close(headers.rstcode | 0); -}); + stream.close(code); +}, tests.length)); server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); @@ -55,9 +61,8 @@ server.listen(0, common.mustCall(() => { countdown.dec(); })); req.on('aborted', common.mustCall()); - if (test[1]) - req.on('error', common.mustCall()); - else - req.on('error', common.mustNotCall()); + req.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, test[1]); + })); }); })); diff --git a/test/parallel/test-http2-server-stream-destroy-after-reset.js b/test/parallel/test-http2-server-stream-destroy-after-reset.js new file mode 100644 index 00000000000000..a0624591c8ebdc --- /dev/null +++ b/test/parallel/test-http2-server-stream-destroy-after-reset.js @@ -0,0 +1,97 @@ +'use strict'; + +// Regression test for the zombie-stream invariant: a server-side +// Http2Stream that receives a peer RST_STREAM MUST eventually be +// destroyed, even when pending `_write` callbacks never fire. This +// surfaced as test-http2-close-while-writing.js flaking on macOS +// (~1 in ~6,700 CI runs); the underlying state is platform-independent +// and produced deterministically here. +// +// Only asserts the invariant ('close' fires within a bounded time). +// The full abort-behavior contract is covered by +// test-http2-reset-aborts-readable-not-drained.js and +// test-http2-reset-happy-close.js. + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const http2 = require('http2'); + +const key = fixtures.readKey('agent8-key.pem', 'binary'); +const cert = fixtures.readKey('agent8-cert.pem', 'binary'); +const ca = fixtures.readKey('fake-startcom-root-cert.pem', 'binary'); + +const server = http2.createSecureServer({ + key, + cert, + // Constrains buffering so the 64 KB body arrives in several DATA frames, + // making the server's 'data' listener fire repeatedly and queue several + // 1-byte writes. + maxSessionMemory: 1000, +}); + +let client; +let clientStream; +let serverStream; + +// If the bug is present the server stream stays a zombie and `'close'` +// never fires, hanging the test until the runner times out. Fail fast +// with a useful diagnostic instead. +const failTimer = setTimeout(() => { + assert.fail( + 'server stream is still a zombie after the peer sent RST_STREAM. ' + + `destroyed=${serverStream?.destroyed} ` + + `closed=${serverStream?.closed} ` + + `aborted=${serverStream?.aborted} ` + + `writableFinished=${serverStream?.writableFinished} ` + + `writableLength=${serverStream?.writableLength}`); +}, common.platformTimeout(5000)); + +server.on('session', common.mustCall((session) => { + session.on('stream', common.mustCall((stream) => { + serverStream = stream; + stream.resume(); + stream.on('data', function() { + // Multiple invocations ⇒ multiple writes ⇒ stranded `_write` + // callbacks once the peer RSTs the stream before they complete. + this.write(Buffer.alloc(1)); + process.nextTick(() => clientStream.destroy()); + }); + + // This regression test only cares that 'close' fires; swallow any + // 'error' the abort path emits. + stream.on('error', () => {}); + + // The actual assertion: the server-side stream must reach 'close' + // (i.e. `_destroy` must run) even though some of its `_write` + // callbacks will never fire. + stream.on('close', common.mustCall(() => { + clearTimeout(failTimer); + assert.strictEqual(stream.destroyed, true); + client.close(); + server.close(); + })); + })); +})); + +server.listen(0, common.mustCall(() => { + client = http2.connect(`https://localhost:${server.address().port}`, { + ca, + maxSessionMemory: 1000, + }); + + clientStream = client.request({ ':method': 'POST' }); + clientStream.resume(); + clientStream.write(Buffer.alloc(64 * 1024)); + + // Deliberately do NOT call client.close()/server.close() here. Doing so + // triggers the graceful-close drain in src/node_http2.cc which can + // cascade-destroy the server-side zombie as a side-effect, masking the + // bug on platforms where that drain fires reliably. Cleanup happens + // from the server stream's 'close' handler above instead. + clientStream.on('close', common.mustCall()); +})); diff --git a/test/parallel/test-http2-server-stream-session-destroy.js b/test/parallel/test-http2-server-stream-session-destroy.js index 4e540e31496668..b14a1f94bfbcf6 100644 --- a/test/parallel/test-http2-server-stream-session-destroy.js +++ b/test/parallel/test-http2-server-stream-session-destroy.js @@ -39,13 +39,13 @@ server.on('stream', common.mustCall((stream) => { name: 'Error' } ); - // When session is destroyed all streams are destroyed and no further - // error should be emitted. + // session.destroy() destroys its streams synchronously; subsequent + // writes fail with ERR_STREAM_DESTROYED via the write callback. stream.on('error', common.mustNotCall()); assert.strictEqual(stream.write('data', common.expectsError({ name: 'Error', - code: 'ERR_STREAM_WRITE_AFTER_END', - message: 'write after end' + code: 'ERR_STREAM_DESTROYED', + message: 'Cannot call write after a stream was destroyed' })), false); })); @@ -53,6 +53,6 @@ server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); const req = client.request(); req.resume(); - req.on('end', common.mustCall()); + req.on('error', () => {}); req.on('close', common.mustCall(() => server.close(common.mustCall()))); }));