Skip to content

node:http2: do not throw Invalid stream id from stream.state and util.inspect(stream) - #43801

Open
robobun wants to merge 4 commits into
mainfrom
robobun/4f33465e/http2-state-getter-closed-stream
Open

robobun wants to merge 4 commits into
mainfrom
robobun/4f33465e/http2-state-getter-closed-stream

Conversation

@robobun

@robobun robobun commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • stream.state throws Invalid stream id on a stream that closed on the wire while its data is unread. util.inspect(stream) reads state, so it throws too. Node v26.3.0 returns state 1 (idle) and zeros.
  • Cause: the native parser drops a closed stream's id at its next socket read (src/runtime/api/bun/h2_frame_parser.rs:3618). get state() (src/js/node/http2.ts) still calls native getStreamState(), which throws for an unknown id.

Fix

  • get state() returns the idle object once the full close has reached JS. Until its next read the native side reports state 7, which node never shows.
  • Native getStreamState() returns undefined for an unknown id. The getter maps that to the idle object, as node's Http2Stream::RefreshState does (node_http2.cc#L3161-L3167).
  • The getter keys on a new bit, NativeCloseSettled, set after the http2.*.stream.close publish. A subscriber still reads live values, as in node.
  • Verified: test/js/node/http2/node-http2.test.js (5 new tests, 3 fail on bun 1.4.3), all of test/js/node/http2/, 27 vendored tests. Self-reviewed: the points raised are addressed (Notes).

Background

  • Node and bun destroy a closed stream only after its consumer reads the buffered data to 'end'.
  • NativeClosed, the existing bit for the full close, is still set before the publish. Session teardown reads it.

Downsides

Notes

Repro. The server responds with 200 and 100 bytes, the client never reads. After two session.ping() round trips:

node v26.3.0:  closed=true destroyed=false | state={"state":1,"weight":0,"sumDependencyWeight":0,"localClose":0,"remoteClose":0,"localWindowSize":0} | inspect ok
bun 1.4.3:     closed=true destroyed=false | state threw: Invalid stream id | inspect threw: Invalid stream id
this PR:       closed=true destroyed=false | state={"state":1,"weight":0,"sumDependencyWeight":0,"localClose":0,"remoteClose":0,"localWindowSize":0} | inspect ok

The same happens on a server stream whose consumer is paused. bun 1.4.2 throws too. bun 1.3.14 does not: it destroyed the stream at the close.

Why the JS check is needed. A setImmediate poll of req.state from 'response' shows the first value after closed turns true:

node v26.3.0:  state 5, then {"state":1, zeros}
bun 1.4.3:     state 5, then state 7 (localClose 1, remoteClose 1, localWindowSize 65535), then the throw after the next read
this PR:       state 5, then {"state":1, zeros}

The native entry stays in state 7 until the next socket read, which can be far away on an idle connection. A native fallback alone still shows state 7 in that window.

Why the native fallback is needed. The status bit is not set on every path that loses the native entry. Each of these threw Invalid stream id with the JS check alone, and now reports the idle object:

  • a pushed client stream at 'stream' and 'push' time (the native stream table has no entry for a promised id),
  • a stream the peer reset, read in the same tick, on a session over a user-supplied Duplex that delivers two chunks in one tick,
  • a stream closed by sendTrailers() on such a session, read after a synchronous peer write and before the deferred close handler.

The close channel. A synchronous http2.client.stream.close or http2.server.stream.close subscriber reads, for a plain request:

node v26.3.0:        {"state":6,"weight":16,"sumDependencyWeight":0,"localClose":1,"remoteClose":1,"localWindowSize":65435}
bun 1.4.3, this PR:  {"state":7,"weight":16,"sumDependencyWeight":0,"localClose":1,"remoteClose":1,"localWindowSize":65535}

NativeClosed is set before the publish, as on main, because destroyStreamForSessionDestroy() and _destroy() read it when a subscriber destroys the session or the stream. A http2.server.stream.close subscriber that calls session.destroy() still leaves a paused stream alive with its unread data, as on main (node destroys that stream). The getter keys on NativeCloseSettled, which markStreamNativeClosed() sets after markStreamClosed() returns, so it still asks the native side during the publish. No other code reads that bit. The test asserts localClose and remoteClose, which node and bun agree on.

Tests. Each part has a test that fails without it. Without the JS check, the two closed-stream tests fail with state 7 at the first assertion. Without the native fallback, the pushed-stream test fails with Invalid stream id. With the getter keyed on NativeClosed, the close-channel test fails. The close() test guards the getter against a check on closed: right after close() the native stream is still live, and node and bun both read state 5. The closed-stream tests assert twice: in the first turn of the event loop that sees closed (before the next socket read), and after two PING round trips. A stream can reach the full close in the same read as the first PING ACK, and the native side drops the id at the start of the next read, so the second ACK proves the drop on both sessions. The same scenarios pass on node v26.3.0 as a standalone script. 20 reruns of the five tests pass on the debug build.

Self-review. The review's summary named these points. The first version of this text said that code which works on node sees no change, which is false for a close-channel subscriber. It said the deferred close handler always runs before the next read, which is false on a Duplex transport. Two throws remained with the bit unset (the pushed stream and the reset stream above). The native getter was not total. The window before the next read had no test. This diff and this text address each of them.

Not changed. In the same tick as sendTrailers(), a stream whose remote side is already closed can read state 7. bun submits the trailers at once and defers only the JS close handling. Node defers the submission, and reads the live half-closed state there (state 6, localClose 0, remoteClose 1). Neither state 7 nor the idle object is node's answer in that tick, so this PR leaves that window alone.

Related PRs. #43754 also guards get state() on NativeClosed and returns {}. This PR returns node's object. The PR that lands second needs a small rebase of that getter. #43433 and #43754 use the two streamEnd lines that now call markStreamNativeClosed() as context.

Found outside this task. Http2Stream#_final reads the status word, publishes http2.client.stream.bodySent, then writes the stale value back (src/js/node/http2.ts, this[bunHTTP2StreamStatus] = status | StreamState.FinalCalled). On a session over a Duplex transport, a subscriber that lets the peer finish makes the full close arrive inside the publish. The write-back then clears Closed and NativeClosed, and stream.closed reads false until the stream is destroyed. No open PR covers it. This PR does not change it.

Suites run with the debug build. test/js/node/http2/node-http2.test.js (395 pass, 6 skip), the other 10 files of test/js/node/http2/ (189 pass), the 18 vendored test-diagnostics-channel-http2-*.js files, and test-http2-session-stream-state, test-http2-stream-client, test-http2-server-stream-session-destroy, test-http2-client-set-priority, test-http2-client-setLocalWindowSize, test-http2-server-setLocalWindowSize, test-http2-no-more-streams, test-http2-respond-file-errors, test-http2-respond-file-fd-errors.


[human-review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 3 failed, 6 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/js/node/http2/node-http2.test.js"
bun test v1.4.3 (367d939d9)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > should be able to send a GET request [735.47ms]
(pass) node none > Client Basics > should be able to send a POST request [496.55ms]
(pass) node none > Client Basics > constants [17.17ms]
(pass) node none > Client Basics > getDefaultSettings [6.86ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [15.45ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [5.05ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [2.85ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [4.51ms]
(pass) node none > Client Basics > should be able to send data using end [517.02ms]
(pass) node none > Client Basics > should be able to mutiplex GET requests [506.47ms]
(pass) node none > Client Basics > http2 should receive remoteSettings when receiving d
... (truncated)

release without fix: 6 skipped
bun test v1.4.3-canary.1 (37ddb3f95)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > constants [0.79ms]
(pass) node none > Client Basics > getDefaultSettings [0.12ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [0.25ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [0.09ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [0.05ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [0.06ms]
(pass) node none > Client Basics > is possible to abort request [1.43ms]
(pass) node none > Client Basics > aborted event should work with abortController [0.65ms]
(pass) node none > Client Basics > aborted event should work with aborted signal [0.61ms]
(pass) node none > Client Basics > signal validation matches node: non-signal objects throw, duck-typed { aborted } is accepted [0.65ms]
(pass) node none > Client Basics > should fail to connect over HTTP/1.1 [26.26ms]
(skip) node none > Client Basics > should not leak memory
(pass) node none > Client Basics > headers cannot be bigge
... (truncated)
passes on PR (with fix)
ASAN with fix: 6 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/js/node/http2/node-http2.test.js"
bun test v1.4.3 (367d939d9)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > should be able to send a GET request [718.74ms]
(pass) node none > Client Basics > should be able to send a POST request [483.97ms]
(pass) node none > Client Basics > constants [16.97ms]
(pass) node none > Client Basics > getDefaultSettings [6.44ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [14.92ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [4.80ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [2.77ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [4.82ms]
(pass) node none > Client Basics > should be able to send data using end [505.46ms]
(pass) node none > Client Basics > should be able to mutiplex GET requests [495.39ms]
(pass) node none > Client Basics > http2 should receive remoteSettings when receiving d
... (truncated)

release with fix: 6 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 818ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/141] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 245 extern-C blocks audited
[2/141] gen cpp.rs (cppbind)
[3/141] gen JS modules (bundle-modules)
Preprocess modules (6811ms)
Bundle modules (40ms)
Postprocesss modules (19ms)
Bundle Functions (412ms)
Generate Code (24ms)

[7.32s] Bundled "src/js" for production
  2596 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[4/26] cxx obj/src/jsc/bindings/BunProcess.cpp.o
[5/26] cxx obj/unified/UnifiedSource-src_jsc_bindings-4.cpp.o
[6/26] cxx obj/unified/UnifiedSource-src_jsc_bindings-1.cpp.o
[7/26] cxx obj/unified/UnifiedSource-src_jsc_modules-0.cpp.o
[8/26] cxx obj/src/jsc/bindings/BunObject.cpp.o
[9/26] cxx obj/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp.o
[10/26] cxx obj/src/jsc/bindings/webcore/streams/JSStreamsRuntime.cpp.o
[11/26] cxx obj/src/jsc/bindings/ZigGlobalObject.cpp.o
[12/26] cxx obj/unified/UnifiedSource-src_jsc_bindings-3.cpp.o
[13/
... (truncated)
diff hotspot
src/js/node/http2.ts                   |  33 +++++-
 src/runtime/api/bun/h2_frame_parser.rs |   3 +-
 test/js/node/http2/node-http2.test.js  | 200 +++++++++++++++++++++++++++++++++
 3 files changed, 230 insertions(+), 6 deletions(-)

gate history · 2 passed · 0 rejected · iteration 0

evidence per changed file
file                                    reads  edits  tests
src/js/node/http2.ts                       13     11     39
src/runtime/api/bun/h2_frame_parser.rs      5      1     39
test/js/node/http2/node-http2.test.js       4      4     39

…e dropped

A stream that closes on the wire while its data is unread stays
undestroyed until the data is read. The native side closes the stream
first and drops its id at the next socket read. Http2Stream#state still
called the native getStreamState(), which throws "Invalid stream id"
for an unknown id. util.inspect(stream) reads state, so it threw too.

The getter now reports what node reports once nghttp2 has dropped a
stream: state 1 (idle) and zeros. It does so from the moment the native
side reports the full close, because node never shows the closed state.
getStreamState() returns undefined for an unknown id, and the getter
maps that to the same idle object. A pushed client stream, which the
native stream table does not hold, no longer throws either.

The NativeClosed bit is now set after the close channel publishes. A
subscriber of http2.*.stream.close still reads the live state there, as
in node.
@robobun

robobun commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review (#43801)

How to reproduce on bun 1.4.3 (node v26.3.0 prints the state object and inspect ok):

import http2 from "node:http2";
import util from "node:util";
const server = http2.createServer();
server.on("stream", s => { s.respond({ ":status": 200 }); s.end(Buffer.alloc(100, 0x61)); });
server.listen(0, "127.0.0.1", () => {
  const client = http2.connect("http://127.0.0.1:" + server.address().port);
  const req = client.request({ ":path": "/" });
  const ping = () => new Promise(r => client.ping(() => r()));
  req.on("response", async () => {
    // The response is complete, but nothing reads it: closed on the wire, not destroyed.
    await ping(); await ping();
    const out = [`closed=${req.closed} destroyed=${req.destroyed}`];
    try { out.push("state=" + JSON.stringify(req.state)); } catch (e) { out.push("state threw: " + e.message); }
    try { util.inspect(req); out.push("inspect ok"); } catch (e) { out.push("inspect threw: " + e.message); }
    console.log(out.join(" | "));
    process.exit(0);
  });
});
bun 1.4.3:  closed=true destroyed=false | state threw: Invalid stream id | inspect threw: Invalid stream id
this PR:    closed=true destroyed=false | state={"state":1,"weight":0,"sumDependencyWeight":0,"localClose":0,"remoteClose":0,"localWindowSize":0} | inspect ok

Test: bun bd test test/js/node/http2/node-http2.test.js -t "Http2Stream#state" (5 tests, 3 of them fail on bun 1.4.3).

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: e7d65c50-3a9b-43a1-80af-7a873c905d36

📥 Commits

Reviewing files that changed from the base of the PR and between d4f100e and 0cb5033.

📒 Files selected for processing (1)
  • src/js/node/http2.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.


Walkthrough

The change centralizes HTTP/2 native stream-close handling, updates post-close state reporting, returns undefined for missing stream state, and adds coverage for client, server, diagnostic, unread, and pushed-stream cases.

Changes

HTTP/2 stream state behavior

Layer / File(s) Summary
Close transition and state reporting
src/js/node/http2.ts, src/runtime/api/bun/h2_frame_parser.rs
Close handlers use a shared transition helper. Http2Stream.state returns native state before and during closure, then returns an idle-state object. Missing streams return undefined.
State behavior validation
test/js/node/http2/node-http2.test.js
Tests cover unread streams, client and server closure, diagnostics-channel visibility, immediate post-close state, and pushed streams.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 0cb50

The HTTP/2 close-state changes present no established merge-blocking risk.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: preventing Invalid stream id errors from Http2Stream.state and util.inspect(stream).
Description check ✅ Passed The description explains the problem, implementation, scope, limitations, related work, and verification results. It does not use the template headings exactly, but it provides the required informatio…

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/js/node/http2.ts — Users still get an 'Invalid stream id' throw from stream.endAfterHeaders in exactly the closed-with-unread-data window this PR fixes for stream.state. The sibling getter at src/js/node/http2.ts:2552-2558 calls native getEndAfterHeaders, which still throws at h2_frame_parser.rs:4942 for an evicted id, and pushed client streams hit the same throw for the stream's whole life. Fix: apply the same treatment to every native per-stream getter in the file (return a JS-side value once NativeClosed is set, and have the native side return undefined for an unknown id), rather than fixing only the getter util.inspect happens to read.

    Why this was flagged

    Trigger: a stream that closed on the wire while its data is unread (the PR's own repro: server responds and the client never reads; after the next socket read the native table drops the id at h2_frame_parser.rs:3618). Reading stream.endAfterHeaders at src/js/node/http2.ts:2555 then calls getEndAfterHeaders, whose lookup at h2_frame_parser.rs:4940-4946 still returns Err(global_object.throw("Invalid stream id")). The same throw occurs for any open pushed client stream because promised ids are never in the table. Base behaves identically, but the PR establishes the invariant that a JS stream outlives its native entry and fixes one of two getters that read the table, leaving the sibling with the exact throw the PR title promises to remove. Population: any code reading endAfterHeaders after a wire close (node returns a boolean there). Remedy: make getEndAfterHeaders total the same way (return undefined -> false), or cache endAfterHeaders as a JS flag at headers time.

    Verification: pre-existing; acknowledged in diff: the PR description's "Downsides" says "stream.endAfterHeaders still throws in the same window. #41945 replaces that getter with a JS flag" — accurate for the closed-with-unread-data window, but it does not mention that pushed client streams hit the same throw for their whole life. Trigger: reading stream.endAfterHeaders on a stream whose native table…

  • 🟣 src/js/node/http2.ts — Server apps that call sendTrailers() and then read stream.state or util.inspect(stream) in the same tick see state 7 with localClose 1 and remoteClose 1, a value node never reports. The streamEnd handler at src/js/node/http2.ts:4116-4122 (client twin at :5120-5126) re-queues the state 7 dispatch on process.nextTick when kSendingTrailers is set, but the native entry is already CLOSED, and NativeClosed is not set until the deferred handler runs. The getter at http2.ts:2515-2519 therefore returns the raw native closed state in that tick. Fix: mark the JS stream natively closed (or return the idle object) whenever the native lookup reports the closed state, so the state 7 window is hidden on every deferral path, not only after the streamEnd handler has run.

    Why this was flagged

    Trigger: ServerHttp2Stream.sendTrailers() at src/js/node/http2.ts:2464-2475 on a stream whose remote side is already closed; the native submit fully closes the stream synchronously and dispatches streamEnd(7), which the handler at :4116-4122 defers with process.nextTick because stream[kSendingTrailers] is true. Until that tick runs the native Stream is in StreamState::CLOSED and the JS status word lacks NativeClosed, so get state() at http2.ts:2515-2519 calls native.getStreamState and returns {state: 7, localClose: 1, remoteClose: 1, ...}. The PR's stated contract (comment at http2.ts:2511-2514 and the PR text: 'node never reports the closed state') is violated on this path; the same happens on a Duplex transport where a peer write lands before the deferred handler, which the PR text itself lists among cases that only the native fallback covers. Population: any server using trailers (gRPC servers send trailers on every response). Remedy: have the getter map a native state of 7 to the idle object, or set the bit at the point the native close is first observed.

    Verification: pre-existing. Trigger: a server stream in HALF_CLOSED_REMOTE (ordinary GET whose request body ended) calls sendTrailers() and then reads stream.state / util.inspect(stream) in the same tick. Mechanism verified in code: native send_trailers at /home/claude/bun/src/runtime/api/bun/h2_frame_parser.rs:5846-5856 synchronously does `stream.state = StreamState::CLOSED;… | pre-existing — the base…

Comment thread src/js/node/http2.ts
Comment thread src/js/node/http2.ts Outdated
A subscriber of http2.*.stream.close can destroy the session from inside
the publish. destroyStreamForSessionDestroy() and _destroy() read the
NativeClosed bit there, so the bit must be set first, as before.

The state getter still has to read the live stream during that publish.
A transient InNativeClose bit covers the publish, and the getter asks
the native side while it is set.
Comment thread src/js/node/http2.ts Outdated
Comment thread src/js/node/http2.ts Outdated
…he publish

NativeClosed stays where it was: set before the close channel publishes.
A second bit, NativeCloseSettled, is set after markStreamClosed()
returns, and only the state getter reads it. The getter needs no
compound check, and markStreamNativeClosed() needs no try/finally.

The idle state object moves into droppedStreamState().
Comment thread src/js/node/http2.ts Outdated
@robobun

robobun commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Replies to the two findings outside the diff. Neither changes code in this PR.

stream.endAfterHeaders still throws. #41945 replaces that getter with a plain JS field that is set at headers time, so it never calls the native side. That covers the closed window, a pushed client stream, and a pending stream. It also keeps node's exact value. A native fallback could only return false, which is wrong for a server stream whose request carried END_STREAM. The Downsides section now names the pushed-stream case too.

state reads 7 in the same tick as sendTrailers(). This is the same on main. In that tick node reads the live half-closed state (state 6, localClose 0, remoteClose 1), because node submits the trailers from setImmediate. bun submits them at once, so the native stream is already closed. Neither state 7 nor the idle object is node's answer there. A getter that maps a native state 7 to the idle object would also blank the http2.*.stream.close window. The native side reports state 7 there, node reports live values, and the new close-channel test covers it. The Notes block of the description records this window under "Not changed".

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the latest push and found no new bugs; the reordering in d4f100e (NativeClosed set before markStreamClosed, NativeCloseSettled after) resolves the session-destroy-from-subscriber path I raised earlier. Since this changes observable node:http2 semantics and shares the get state() getter with the in-flight #43754, a human look before merge would still be worthwhile.

What was reviewed:

  • getStreamState throw -> undefined: the getter at src/js/node/http2.ts:2526 is the only JS caller, and the only test asserting "Invalid stream id" (node-http2-streams-rehash.test.ts) targets getStreamContext, so nothing else depended on the throw.
  • droppedStreamState() field values match node's Http2Stream::RefreshState null-stream branch (IDLE, all others 0); the {} results for missing native handle / no id / destroyed session are preserved.
  • The NativeCloseSettled bit is consumed only by the getter and set only via markStreamNativeClosed, which both state==7 handlers (client and server) now use.
Extended reasoning...

The change touches src/js/node/http2.ts (a new StreamState bit, a markStreamNativeClosed helper, and a reworked Http2Stream#state getter), a one-line return change in src/runtime/api/bun/h2_frame_parser.rs, and five new tests in test/js/node/http2/node-http2.test.js. It touches no auth, crypto, or input-parsing surface; the Rust change only swaps a throw for an undefined return on a missing map entry. The source diff is small and the ordering concern from the previous review was addressed by a follow-up commit. Deferring rather than approving because the PR itself documents a remaining deviation from node for open pushed streams (delegated to another PR) and a getter overlap with #43754 that a maintainer should sequence, and because no debug build was available here to rerun the suite.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant