fix(http2): close a streaming response body on every exit path #13

Merged
Relism merged 1 commits from hotfix/http2-response-writer-stream-leak into master 2026-08-14 23:02:30 +00:00
Owner

Summary

Http2ResponseWriter.streamBody (a streaming response's InputStream, driven across startFlowControlled/resume as flow-control windows allow) was never closed anywhere — not on clean EOF, not on a write failure, not when the stream is abandoned (RST_STREAM from the peer, connection teardown). Same bug Http1ResponseWriter had before cf16be0, just never given the same fix: a handler stream that releases a held resource (a pooled backend connection, for a reverse proxy) from close() leaks it under any real amount of stream resets or aborted connections.

Fix

  • appendData closes streamBody once end is reached (clean completion) and on any IOException from the read itself, mirroring Http1ResponseWriter's relayAndClose/writeChunkedAndClose reasoning.
  • New Http2ResponseWriter#abort(), called from Http2Stream#cancel() — symmetric with that method's existing http2Body.cancel() for the inbound leg, now covering the outbound one too. cancel() is the single hook every abandoned-stream path (RST_STREAM handling and connection teardown in Http2Connection, plus Http2StreamDispatcher) already goes through, so this covers every abort case without adding a new one.
  • closeStreamBodyQuietly() is idempotent (nulls streamBody after closing), so the appendData and abort() close paths can't double-close or race.

Test plan

  • Four new Http2ResponseWriterTest cases: normal completion in one call, completion across a resume() (multiple flow-control windows), abort() while still streaming, and abort() as a no-op on a non-streaming response
  • Full flash module suite: 697/697 green
## Summary `Http2ResponseWriter.streamBody` (a streaming response's `InputStream`, driven across `startFlowControlled`/`resume` as flow-control windows allow) was never closed anywhere — not on clean EOF, not on a write failure, not when the stream is abandoned (RST_STREAM from the peer, connection teardown). Same bug `Http1ResponseWriter` had before `cf16be0`, just never given the same fix: a handler stream that releases a held resource (a pooled backend connection, for a reverse proxy) from `close()` leaks it under any real amount of stream resets or aborted connections. ## Fix - `appendData` closes `streamBody` once `end` is reached (clean completion) and on any `IOException` from the read itself, mirroring `Http1ResponseWriter`'s `relayAndClose`/`writeChunkedAndClose` reasoning. - New `Http2ResponseWriter#abort()`, called from `Http2Stream#cancel()` — symmetric with that method's existing `http2Body.cancel()` for the inbound leg, now covering the outbound one too. `cancel()` is the single hook every abandoned-stream path (RST_STREAM handling and connection teardown in `Http2Connection`, plus `Http2StreamDispatcher`) already goes through, so this covers every abort case without adding a new one. - `closeStreamBodyQuietly()` is idempotent (nulls `streamBody` after closing), so the `appendData` and `abort()` close paths can't double-close or race. ## Test plan - [x] Four new `Http2ResponseWriterTest` cases: normal completion in one call, completion across a `resume()` (multiple flow-control windows), `abort()` while still streaming, and `abort()` as a no-op on a non-streaming response - [x] Full `flash` module suite: 697/697 green
Relism added 1 commit 2026-08-14 22:58:19 +00:00
Http2ResponseWriter.streamBody (a streaming response's InputStream, driven
across startFlowControlled/resume as flow-control windows allow) was never
closed anywhere -- not on clean EOF, not on a write failure, not when the
stream is abandoned (RST_STREAM from the peer, connection teardown). Same
bug Http1ResponseWriter had before cf16be0, just never given the same fix: a
handler stream that releases a held resource (a pooled backend connection,
for a reverse proxy) from close() leaks it under any real amount of stream
resets or aborted connections.

- appendData closes streamBody once `end` is reached (clean completion) and
  on any IOException from the read itself, mirroring Http1ResponseWriter's
  relayAndClose/writeChunkedAndClose reasoning.
- New Http2ResponseWriter#abort(), called from Http2Stream#cancel() --
  symmetric with that method's existing http2Body.cancel() for the inbound
  leg, now covering the outbound one too. cancel() is the single hook every
  abandoned-stream path (RST_STREAM handling and connection teardown in
  Http2Connection, plus Http2StreamDispatcher) already goes through, so this
  covers every abort case without adding a new one.

closeStreamBodyQuietly() is idempotent (nulls streamBody after closing), so
the appendData and abort() close paths can't double-close or race.

Four new Http2ResponseWriterTest cases: normal completion in one call,
completion across a resume() (multiple flow-control windows), abort() while
still streaming, and abort() as a no-op on a non-streaming response. 697/697
flash-module tests green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LLwcyHUnbApCrY33gvgoa
Relism merged commit f8e0a1d3fa into master 2026-08-14 23:02:30 +00:00
Relism deleted branch hotfix/http2-response-writer-stream-leak 2026-08-14 23:02:30 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Relism/Flash5#13