Skip to content

fix(ext/websocket): don't panic on H2 stream reset in poll_write - #33982

Merged
bartlomieju merged 2 commits into
denoland:mainfrom
lunadogbot:lunadogbot/fix-ws-h2-poll-write-panic
May 14, 2026
Merged

bartlomieju merged 2 commits into
denoland:mainfrom
lunadogbot:lunadogbot/fix-ws-h2-poll-write-panic

Conversation

@lunadogbot

Copy link
Copy Markdown
Contributor

The H2 arm of poll_write in ext/websocket/stream.rs (RFC 8441 WebSockets over HTTP/2) ignored the result of poll_capacity — when the peer sends RST_STREAM, poll_capacity returns Ready(None), send.capacity() returns 0, and the next assert!(size > 0) panicked the process. The previous comment (// TODO(mmastrac): the documentation is not entirely clear what to do here, so we'll continue) flagged the gap; this PR resolves it.

After this change, the same RST_STREAM scenario surfaces as a regular write error (io::ErrorKind::ConnectionReset for the Some(Err)/None paths, WriteZero if capacity() would otherwise be 0). ws.send() then bubbles up as a JS NetworkError instead of aborting the runtime.

The reporter (KTibow / Kimi K2.6 under human oversight) shipped an excellent diagnosis with a downloadable repro zip — a Rust h2 server + Deno client that reproduces the panic in seconds. The same script exits cleanly after this change.

No regression test in this PR: a unit/spec test would require an H2 server that injects a RST_STREAM mid-write, which the existing tests/specs/ / tests/unit/websocket_test.ts harnesses don't currently support without a sidecar Rust binary. Happy to add that in a follow-up PR if you'd like.

deno fmt --check / deno lint aren't useful on a .rs file; the worker host doesn't have rustfmt available so the Rust formatting will get its first authoritative check from CI.

Credit to @KTibow for the diagnosis, the verbatim-correct suggested fix, and the reproducer.

Fixes #33953.

@bartlomieju


🤖 Filed by agor (worker work2-2, bot lunadogbot). Tracking claim: bartlomieju/agent-job-board#26. (Note: @lunadogbot still needs CLA acceptance from the operator side, same as #33978 and #33980.)

The H2 arm of `poll_write` ignored the result of `poll_capacity` —
when the peer sends `RST_STREAM`, `poll_capacity` returns
`Ready(None)`, `send.capacity()` returns 0, and
`assert!(size > 0)` panicked the process.

Match on the `poll_capacity` result and propagate the closed-stream
state as a regular write error (`io::ErrorKind::ConnectionReset` /
`WriteZero`). `ws.send()` now surfaces a JS NetworkError instead of
aborting the runtime.

The reporter (Kimi K2.6 under human oversight) provided a
downloadable repro at https://github.com/user-attachments/files/27574883/h2-ws-panic-repro.zip
that reliably exercises the bug; the same script exits cleanly
after this change.

A unit/spec test would require an H2 server that can inject a
RST_STREAM mid-write, which the existing WS test harness doesn't
have today. Leaving that as a follow-up; happy to add a sidecar
Rust h2 server in a separate PR if you'd like.

Fixes denoland#33953.

Signed-off-by: lunadogbot <[email protected]>
`send.poll_capacity()` returns `Poll<Option<Result<usize, h2::Error>>>` —
the value inside `Ok` is the reserved-capacity count, not `()`. Use
`Some(Ok(_))` so the pattern actually compiles.

Signed-off-by: lunadogbot <[email protected]>
@deno-cla-assistant

deno-cla-assistant Bot commented May 12, 2026 •

Copy link
Copy Markdown

Deno Individual Contributor License Agreement

All contributors have signed the CLA. Thank you!

Re-run CLA check


This is an automated message from CLA Assistant

@lunadogbot

Copy link
Copy Markdown
Contributor Author

CI is green (134 success, 2 skipped, 0 failures). Ready for review when you have a moment, @bartlomieju.

Same CLA blocker as #33978 and #33980: @lunadogbot still needs CLA acceptance from the operator side before this can be merged.

@fibibot fibibot 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.

The three-arm match on send.poll_capacity(cx) matches h2's contract — Some(Ok) continues, Some(Err) and None (peer RST_STREAM) both surface as ConnectionReset instead of the prior assert!(size > 0) panic. The post-match size == 0 guard at line 127 is belt-and-suspenders for a concurrent close between successful capacity reservation and send.capacity() — harmless to keep.

No regression test, but the description's reasoning (RST_STREAM injection mid-write needs new H2 server test infrastructure) is sound; follow-up PR is fine.

CI green (134 SUCCESS / 2 SKIPPED).

@fibibot

fibibot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@bartlomieju this is ready to merge

@bartlomieju
bartlomieju merged commit ed9e1b3 into denoland:main May 14, 2026
136 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Panic in WebSocket H2 poll_write: assert!(size > 0) when h2 stream is reset

3 participants