Skip to content

fix(compression): avoid re-polling body after EOF in BodyIntoStream - #734

Open
zfaustk wants to merge 1 commit into
tower-rs:mainfrom
zfaustk:fix-compression-repoll-eof
Open

zfaustk wants to merge 1 commit into
tower-rs:mainfrom
zfaustk:fix-compression-repoll-eof

Conversation

@zfaustk

@zfaustk zfaustk commented Sep 6, 2026

Copy link
Copy Markdown

Motivation

Closes #732

When compressing a streaming response body backed by a non-fused stream (e.g. Body::from_stream(futures::stream::unfold(...))), Compression panics at the end of the stream with:
Unfold must not be polled after it returned Poll::Ready(None).

In BodyIntoStream, poll_next drives the underlying body to read all data frames into the encoder. Once this.body.poll_frame(cx) returns None, yielded_all_data is set. After the encoder finishes writing all compressed output, WrapBody::poll_frame calls BodyIntoStream::poll_frame to forward any remaining frames (such as trailers).

If no trailers were buffered, BodyIntoStream::poll_frame unconditionally fell through to this.body.poll_frame(cx). For bodies backed by non-fused streams, re-polling an already-exhausted body violates the stream contract and panics.

Solution

Track whether the inner body has already returned None via a boolean field (body_eof). When body_eof is true, BodyIntoStream::poll_frame immediately returns Poll::Ready(None) instead of re-polling the inner body.

When wrapping a body into a stream for compression, BodyIntoStream
polls the underlying body to completion (None) to read data frames into
the encoder. WrapBody then invokes poll_frame to collect any remaining
trailers or non-data frames.

Previously, poll_frame unconditionally re-polled the inner body if no
trailers were buffered. When the underlying body is backed by a non-fused
stream (such as futures::stream::unfold), this re-poll panics after EOF.

Track when the inner body has yielded EOF (None), and return Ready(None)
directly rather than re-polling the inner body.

Fixes tower-rs#732.

@cratelyn cratelyn left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

thank you for opening a pull request @zfaustk. i appreciate that this commit includes a test case too.

i have one suggestion about how we can patch this in a more efficient manner, without introducing this body_eof boolean.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if we want to avoid polling the underlying body after it yields Ready(None), i think that it would be more prudent to use http_body_util::combinators::Fuse<B>.

we can wrap B in that combinator, rather than reimplementing the logic for fusing a body here.

Comment on lines 289 to +291
yielded_all_data: false,
non_data_frame: None,
body_eof: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

it smells weird to me that this introduces a body_eof flag when there is already a yielded_all_data flag here. how do these differ?

@tower-rs tower-rs deleted a comment from De-ASI-INTERFACE Sep 15, 2026
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.

CompressionLayer re-polls non-fused streaming bodies after EOF and panics

4 participants