NewsOpen Source

Rust’s hyper Bug Silently Drops Data: Check Your Cargo.lock

Rust Ferris crab logo with HTTP response data fragments dissolving and dropping away, illustrating the hyper silent data-loss bug

On June 22, 2026, Cloudflare published a post-mortem on a bug they discovered in hyper — Rust’s most widely used low-level HTTP library — that had been silently dropping response data across multiple major versions. The rust hyper bug causes HTTP servers to truncate large responses while returning HTTP 200 and correct Content-Length headers. No errors are logged on the server. No errors surface on the client. Data just disappears.

hyper underpins axum (the dominant Rust web framework), reqwest (the most popular Rust HTTP client), and tonic (Rust’s gRPC stack). If you run a Rust HTTP server and serve responses larger than a few hundred kilobytes, you may have been silently corrupting data in production for years without knowing it. Check your Cargo.lock before reading further.

The Rust hyper Bug That `let _` Hid

The culprit is in hyper/src/proto/h1/dispatch.rs, in the HTTP/1 connection dispatch loop. Here is the relevant code before the fix:

fn poll_loop(&mut self, cx: &mut Context) -> Poll<Result> {
    loop {
        let _ = self.poll_read(cx)?;
        let _ = self.poll_write(cx)?;
        let _ = self.poll_flush(cx)?;  // Poll::Pending silently dropped
        if !self.conn.wants_read_again() {
            return Poll::Ready(Ok(()));
        }
    }
}

The problem is let _ = self.poll_flush(cx)?. In Rust’s async model, poll_flush returns Poll::Pending when the kernel’s socket buffer is full and not all data has been written to it. That return value is the async system’s way of saying “I’m not done yet — come back later.” Discarding it with let _ tells the runtime the opposite: “done, move on.” The connection proceeds to shutdown, leaving megabytes of response data stranded in hyper’s internal buffer with nowhere to go.

The bug only triggers when socket buffers fill under load — which requires large response bodies and real network conditions. Small responses that fit inside a single socket buffer are never affected. This is precisely why the bug stayed hidden: most test environments don’t reproduce the backpressure conditions that expose it.

Six Weeks to Find a Four-Line Fix

Cloudflare engineers Deanna Lam, Diretnan Domnan, and Matt Lewis spent six weeks hunting this down after customer reports of truncated image responses from their Images binding service. Every hypothesis failed. Timeout configurations looked fine. Upgrading hyper versions changed nothing. Local reproduction attempts came up empty — the failure only materialized under production network conditions.

The breakthrough was strace. Watching raw syscalls, the team saw the failure pattern starkly: a 14.99 MB response would produce a single sendto() call pushing 219 KB, followed immediately by shutdown(42, SHUT_WR) = 0. The socket closed with 14.77 MB still unsent, and neither end reported an error. Working requests, by contrast, issued dozens of sendto() calls until the buffer drained before shutdown arrived.

Six weeks. No error logs. No panics. Just customers receiving truncated images and an HTTP 200 status. If Cloudflare’s SRE team with strace and distributed tracing infrastructure needed six weeks, smaller teams running Rust HTTP services may never diagnose this at all.

Related: crustc: The Entire Rust Compiler, Translated to C

Who’s Affected and How to Fix the hyper Data Loss

Affected versions: hyper 0.14.x through 1.8.x. The fix — PR #4018 — is four lines, moving the flush guarantee into poll_shutdown() using the ready!() macro to block connection shutdown until the flush actually completes:

pub(crate) fn poll_shutdown(
    &mut self,
    cx: &mut Context,
) -> Poll<io::Result> {
    ready!(self.poll_flush(cx)?);    // Block until flush completes
    Pin::new(&mut self.io).poll_shutdown(cx)
}

Check your Cargo.lock for the hyper entry and compare against the hyper changelog on crates.io for the first version shipping the PR #4018 fix. Since axum and reqwest both pull in hyper as a transitive dependency, you may be on an affected version without realizing it. Cloudflare deployed an internal fork with the patch while awaiting the official release.

Memory Safe Doesn’t Mean Logically Correct

The broader lesson here is one the Rust community occasionally needs to hear plainly: Rust’s safety guarantees cover memory safety — no buffer overflows, no use-after-free, no data races. They do not cover logical correctness in async code. Discarding a Poll::Pending return value is syntactically valid Rust. The compiler cannot stop you. Nothing warns you that you’ve violated the async contract.

Moreover, the let _ pattern is idiomatic Rust for suppressing unused-variable warnings and is benign in most contexts. In async code, however, Poll values carry semantic contracts that discarding can silently break. That gap — between what the type system enforces and what async correctness actually requires — is real. This bug lived in one of the ecosystem’s most foundational crates for years because nothing in the language automatically catches it. The Clippy linter could theoretically add a lint for discarded Poll values in async contexts; until then, code review is the only guard.

Key Takeaways

  • hyper 0.14.x through 1.8.x silently truncates large HTTP responses due to a discarded poll_flush return value — check your Cargo.lock immediately.
  • The failure manifests as HTTP 200 with correct Content-Length but a truncated body, with no errors logged on either side. It surfaces only with large responses under real network backpressure.
  • The fix is in PR #4018. Upgrade to a patched hyper version; axum and reqwest users must verify their transitive hyper dependency is updated too.
  • Rust’s memory safety doesn’t extend to async correctness. Logical errors in async code — like discarding Poll::Pending — compile cleanly and can cause silent data corruption.
ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:News