fix(http1): flush buffered data on shutdown even when pipeline-flushing - #1
Open
fotinakis wants to merge 1 commit into
Open
fix(http1): flush buffered data on shutdown even when pipeline-flushing#1fotinakis wants to merge 1 commit into
fotinakis wants to merge 1 commit into
Conversation
fotinakis
force-pushed
the
fix/http1-flush-buffered-before-shutdown
branch
from
July 13, 2026 02:25
9507636 to
823dc79
Compare
`Buffered::poll_shutdown` reuses `poll_flush` to drain any committed response bytes before closing the connection (added in hyperium#4018). But `poll_flush` short-circuits to `Ready(Ok(()))` whenever the pipeline-flush optimization is enabled and there are still unparsed request bytes in the read buffer: if self.flush_pipeline && !self.read_buf.is_empty() { Poll::Ready(Ok(())) } So when a server is built with `pipeline_flush(true)` and a client pipelines another request into the read buffer, the flush inside `poll_shutdown` becomes a no-op and the connection is torn down with response bytes still sitting in the write buffer. The client gets a truncated (or entirely missing) response even though the status line and Content-Length claimed a complete one. This is the same failure mode as hyperium#4022, reached through the pipeline-flush path that hyperium#4018 didn't cover. A client pipelining after a `Connection: close` request is enough to hit this ("SHOULD NOT", not "MUST NOT" -- RFC 9112 section 9.6). Split the actual buffer-draining out of `poll_flush` into `poll_flush_buffered`, which ignores the `flush_pipeline` short-circuit, and have `poll_shutdown` call it. `poll_flush`'s behavior is unchanged. Adds a unit test that puts a `Buffered` in the exact short-circuit state (pipeline flushing on, a pipelined request in the read buffer, a committed response in the write buffer) and asserts shutdown flushes it first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pipeline_flush(true)can cause an HTTP/1 server to close a connection with a committed response still buffered, truncating (or entirely dropping) it. This is the same failure mode as hyperium#4022, but reached through a code path that the hyperium#4018 fix doesn't cover.Background
hyperium#4018 fixed hyperium#4022 by making
Buffered::poll_shutdownflush any buffered response bytes before closing:The catch is that
poll_flushhas a short-circuit for the pipeline-flush optimization:That short-circuit is correct for a normal flush (hold off so responses to already-queued requests get batched into one write), but
poll_shutdowninherits it. So when a server is built withpipeline_flush(true)and there are still unparsed request bytes in the read buffer, the flush insidepoll_shutdownbecomes a no-op and the connection is torn down with the response still inwrite_buf.The client sees a status line and
Content-Lengthpromising a full response, a clean connection close, and a short body — no error anywhere.How to hit it
pipeline_flush(true).read_bufis non-empty when the connection goes to close. Pipelining after aConnection: closerequest is enough — that's a "SHOULD NOT", not a "MUST NOT" (RFC 9112 §9.6).Fix
Split the actual buffer-draining out of
poll_flushintopoll_flush_buffered, which ignores theflush_pipelineshort-circuit, and havepoll_shutdowncall that instead.poll_flush's behavior is unchanged; only shutdown is affected.Test
Adds a
Bufferedunit test that puts the connection in exactly the short-circuit state — pipeline flushing on, a pipelined request in the read buffer, a committed response in the write buffer — and asserts shutdown flushes the response before closing. It fails onmaster("data left to write") and passes with the fix.Notes
Found while building a small deterministic simulation harness around the HTTP/1 server (the same kind of setup that surfaced hyperium#4022): randomizing socket buffer sizes, request pipelining, and hyper's own config knobs, with an invariant that every 200 response must deliver exactly
Content-Lengthbytes. Withpipeline_flushin the mix this reproduced across a large fraction of seeds; the fix clears all of them.