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
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 master2026-08-14 23:02:30 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Http2ResponseWriter.streamBody(a streaming response'sInputStream, driven acrossstartFlowControlled/resumeas 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 bugHttp1ResponseWriterhad beforecf16be0, just never given the same fix: a handler stream that releases a held resource (a pooled backend connection, for a reverse proxy) fromclose()leaks it under any real amount of stream resets or aborted connections.Fix
appendDataclosesstreamBodyonceendis reached (clean completion) and on anyIOExceptionfrom the read itself, mirroringHttp1ResponseWriter'srelayAndClose/writeChunkedAndClosereasoning.Http2ResponseWriter#abort(), called fromHttp2Stream#cancel()— symmetric with that method's existinghttp2Body.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 inHttp2Connection, plusHttp2StreamDispatcher) already goes through, so this covers every abort case without adding a new one.closeStreamBodyQuietly()is idempotent (nullsstreamBodyafter closing), so theappendDataandabort()close paths can't double-close or race.Test plan
Http2ResponseWriterTestcases: normal completion in one call, completion across aresume()(multiple flow-control windows),abort()while still streaming, andabort()as a no-op on a non-streaming responseflashmodule suite: 697/697 green