Skip to content

Commit f660f5b

Browse files
authored
fix(http1): flush bytes buffered by the write re-check before yielding (#4143)
`poll_loop`'s main path always calls `poll_flush` after `poll_write`. The "wants_write_again" re-check added in #3988 calls `poll_write` a second time and returns straight out of the loop when it pends, skipping that flush. That second write can buffer bytes before it pends. When a response body reaches end-of-stream between the two write polls, `end_body()` buffers the end of the message and the write then pends on the *next* message (`poll_msg`). Returning there strands the terminating chunk in the write buffer: the wake-ups the connection is left waiting on are for reads, so nothing flushes it. The peer receives the body but never the terminator and waits until it gives up, at which point the connection reports `IncompleteMessage` from `mid_message_detect_eof`. Observed on a server streaming a chunked body fed from another thread, at roughly one connection in 600k. hyper's own trace shows the divergence: healthy: buf.len=24, buf.len=5, flushed 29 bytes stalled: buf.len=24, flushed 24 bytes, buf.len=5, <nothing> Flush what the re-check buffered before yielding. Guard the flush on there being buffered bytes so the call pattern is otherwise unchanged. Add a test that drives the interleaving deterministically: a body that yields one data frame, then pends, then ends the stream on the very next poll, all within a single `poll_loop` iteration.
1 parent 8af6787 commit f660f5b

4 files changed

Lines changed: 137 additions & 0 deletions

File tree

src/proto/h1/conn.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ where
592592
self.io.can_buffer()
593593
}
594594

595+
/// Whether bytes are sitting in the write buffer waiting to be flushed.
596+
pub(crate) fn has_buffered_write(&self) -> bool {
597+
self.io.has_buffered_write()
598+
}
599+
595600
pub(crate) fn write_head(&mut self, head: MessageHead<T::Outgoing>, body: Option<BodyLength>) {
596601
if let Some(encoder) = self.encode_head(head, body) {
597602
self.state.writing = if !encoder.is_eof() {

src/proto/h1/dispatch.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ where
205205
// we need to check it again. If it is still pending, it is safe to yield and rely
206206
// on wake-up from the connection futures.
207207
if self.poll_write(cx)?.is_pending() {
208+
// That write can have buffered bytes before going pending: a body that
209+
// reached end-of-stream between the two write polls buffers the end of the
210+
// message here, and then the write goes pending on the *next* message.
211+
// Yielding without flushing would strand those bytes in the write buffer
212+
// until the peer gives up, since the wake-ups we then rely on are for
213+
// reads. Flush what was just buffered before yielding.
214+
if self.conn.has_buffered_write() {
215+
let _ = self.poll_flush(cx)?;
216+
}
208217
return Poll::Ready(Ok(()));
209218
}
210219
}

src/proto/h1/io.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ where
149149
self.write_buf.buffer(buf);
150150
}
151151

152+
/// Whether there are bytes waiting in the write buffer to be flushed.
153+
pub(crate) fn has_buffered_write(&self) -> bool {
154+
self.write_buf.remaining() > 0
155+
}
156+
152157
pub(crate) fn can_buffer(&self) -> bool {
153158
self.flush_pipeline || self.write_buf.can_buffer()
154159
}

tests/h1_flush_before_yield.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Test: `poll_loop` must never yield leaving buffered bytes unflushed.
2+
//
3+
// `poll_loop`'s main path always calls `poll_flush` after `poll_write`. The
4+
// "wants_write_again" re-check added a *second* `poll_write` whose `Pending`
5+
// returned straight out of the loop, skipping that flush.
6+
//
7+
// That second write can buffer bytes before it pends. A response body that
8+
// reaches end-of-stream between the two write polls has its end-of-message
9+
// buffered by `end_body()`, and the write then pends on the *next* message
10+
// (`poll_msg`). Returning there strands the terminating chunk in the write
11+
// buffer: the wake-ups the connection then waits on are for reads, so nothing
12+
// flushes it and the peer waits for a response that is already written but
13+
// never sent.
14+
//
15+
// The body below drives exactly that interleaving deterministically: one data
16+
// frame, then `Pending`, then end-of-stream on the very next poll, both of
17+
// which happen inside a single `poll_loop` iteration.
18+
19+
use std::convert::Infallible;
20+
use std::pin::Pin;
21+
use std::task::{Context, Poll};
22+
use std::time::Duration;
23+
24+
use bytes::Bytes;
25+
use hyper::body::{Body, Frame};
26+
use hyper::server::conn::http1;
27+
use hyper::service::service_fn;
28+
use hyper::Response;
29+
use support::TokioIo;
30+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
31+
use tokio::net::{TcpListener, TcpStream};
32+
use tokio::time::timeout;
33+
34+
mod support;
35+
36+
/// Yields one data frame, then pends once, then ends the stream.
37+
///
38+
/// The `Pending` deliberately arranges no wake-up: it stands in for a body
39+
/// whose readiness changes between hyper's two write polls of the same
40+
/// `poll_loop` iteration, which is what leaves the end of the message buffered
41+
/// by the re-check write.
42+
#[derive(Default)]
43+
struct PendOnceThenEnd {
44+
polls: u8,
45+
}
46+
47+
impl Body for PendOnceThenEnd {
48+
type Data = Bytes;
49+
type Error = Infallible;
50+
51+
fn poll_frame(
52+
mut self: Pin<&mut Self>,
53+
_cx: &mut Context<'_>,
54+
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
55+
self.polls += 1;
56+
match self.polls {
57+
1 => Poll::Ready(Some(Ok(Frame::data(Bytes::from_static(b"hello"))))),
58+
2 => Poll::Pending,
59+
_ => Poll::Ready(None),
60+
}
61+
}
62+
}
63+
64+
#[tokio::test]
65+
async fn h1_server_flushes_end_of_body_buffered_by_write_recheck() {
66+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
67+
let addr = listener.local_addr().unwrap();
68+
69+
tokio::spawn(async move {
70+
let (socket, _) = listener.accept().await.unwrap();
71+
let service = service_fn(|_req| async {
72+
Ok::<_, Infallible>(Response::new(PendOnceThenEnd::default()))
73+
});
74+
let _ = http1::Builder::new()
75+
.serve_connection(TokioIo::new(socket), service)
76+
.await;
77+
});
78+
79+
let mut client = TcpStream::connect(addr).await.unwrap();
80+
client
81+
.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
82+
.await
83+
.unwrap();
84+
85+
// The whole response must arrive, terminating chunk included. Before the
86+
// fix the body's chunk arrives but `0\r\n\r\n` never does, so this read
87+
// waits forever.
88+
let mut received = Vec::new();
89+
let read = timeout(Duration::from_secs(5), async {
90+
let mut buf = [0u8; 256];
91+
loop {
92+
let n = client.read(&mut buf).await.unwrap();
93+
if n == 0 {
94+
break;
95+
}
96+
received.extend_from_slice(&buf[..n]);
97+
if received.ends_with(b"\r\n0\r\n\r\n") {
98+
break;
99+
}
100+
}
101+
})
102+
.await;
103+
104+
assert!(
105+
read.is_ok(),
106+
"response never completed; got {:?}",
107+
String::from_utf8_lossy(&received)
108+
);
109+
let received = String::from_utf8_lossy(&received);
110+
assert!(
111+
received.ends_with("\r\n0\r\n\r\n"),
112+
"missing terminating chunk; got {received:?}"
113+
);
114+
assert!(
115+
received.contains("hello"),
116+
"missing body content; got {received:?}"
117+
);
118+
}

0 commit comments

Comments
 (0)