Skip to content

fix(tonic-web): don't drop an already-parsed trailer when it arrives alongside data in one poll#2727

Open
a5x10 wants to merge 1 commit into
grpc:masterfrom
a5x10:fix/grpc-web-trailer-dropped-when-buffered-with-data
Open

fix(tonic-web): don't drop an already-parsed trailer when it arrives alongside data in one poll#2727
a5x10 wants to merge 1 commit into
grpc:masterfrom
a5x10:fix/grpc-web-trailer-dropped-when-buffered-with-data

Conversation

@a5x10

@a5x10 a5x10 commented Jul 9, 2026

Copy link
Copy Markdown

Bug

GrpcWebCall::poll_frame's client-side decode branch (tonic-web/src/call.rs) correctly parses and stashes a trailer frame into self.trailers when it arrives in the same poll_frame call as preceding data — e.g. a fully-buffered response, or any transport that happens to deliver a small response in one read.

But the immediately-following poll_frame call (required by the http_body::Body contract before the caller treats the stream as ended) finds the accumulation buffer empty, takes the FindTrailers::Done(0) arm, and returns None unconditionally, without ever checking self.trailers:

FindTrailers::Done(len) => Poll::Ready(match len {
    0 => None,                                                    // <-- drops an already-parsed trailer
    _ => Some(Ok(Frame::data(buf.split_to(len).freeze()))),
}),

The already-parsed trailer is silently dropped. A tonic client sees:

missing grpc-status trailer, stream was terminated without a final status (possible truncation by a proxy or load balancer)

even though the server sent a perfectly well-formed grpc-web response.

Compare the FindTrailers::Trailer(len) arm a few lines above, which does correctly fall back to checking self.trailers when its own leading data is empty — Done(0) has no equivalent fallback. That asymmetry is the actual bug, not a buffering/timing edge case.

Repro

Found via a real production failure (a tonic-based CLI's grpc-web client against a real deployment). Root-caused offline: captured the actual bytes of a real 232-byte production response, confirmed by hand that they're a perfectly well-formed grpc-web reply (one data frame immediately followed by a complete trailer frame), then reproduced the drop with a minimal, network-free test — included in this PR as call::tests::trailer_not_dropped_when_buffered_with_data. The new test fails without this fix and passes with it (verified both ways before opening this PR).

Fix

One arm, mirroring the FindTrailers::Trailer(len) arm's own existing fallback: check self.trailers.take() before returning None.

Full tonic-web test suite (22 passed, 1 pre-existing #[ignore]d unrelated test, 0 failed) and cargo fmt --check both clean on this branch.

… poll_frame

GrpcWebCall::poll_frame's client+Decode branch correctly parses and
stashes a trailer into self.trailers when it arrives in the same
poll_frame call as preceding data (e.g. a fully-buffered response, or
any transport that happens to deliver a small response in one read).
But the immediately following poll_frame call - required by the
http_body::Body contract before the caller treats the stream as ended
- finds the accumulation buffer empty, takes the
FindTrailers::Done(0) arm, and returns None unconditionally, without
ever checking self.trailers. The already-parsed trailer is silently
dropped; a tonic client sees "missing grpc-status trailer, stream was
terminated without a final status" even though the server sent a
perfectly well-formed response.

Fix mirrors the FindTrailers::Trailer(len) arm's own existing
fallback a few lines above: check self.trailers.take() before
returning None.

Found via a real production failure (esteem CLI's grpc-web client
against a real deployment) - root-caused offline with a byte-exact
capture of an actual 232-byte production response, confirmed the
bytes are a perfectly well-formed grpc-web reply, and isolated the
bug with a minimal, network-free repro (included as the new
regression test) before touching this crate at all.
@linux-foundation-easycla

Copy link
Copy Markdown

CLA Missing ID

  • ❌ The email address for the commit (23b5303) is not linked to the GitHub account, preventing the EasyCLA check. Consult this Help Article and GitHub Help to resolve. (To view the commit's email address, add .patch at the end of this PR page's URL.) For further assistance with EasyCLA, please visit our EasyCLA portal and chat with our support bot.

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.

1 participant