Description
Version
hyper 0.14.26
tokio 1.28.1
Platform
Linux my-hostname 5.10.0-18-amd64 #1 SMP Debian 5.10.140-1 (2022-09-02) x86_64 GNU/Linux
Description
Error is not propagated via stream in some cases when client drops the connection during chunked-encoding stream.
I tried this code:
use futures_util::stream::StreamExt;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
async fn echo(mut req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
println!("reading body!");
// Create a channel to communicate between the reading and responding tasks
let (tx, rx) = mpsc::channel::<Vec<u8>>(1);
// Spawn a task to read the request body and discard it
tokio::spawn(async move {
while let Some(maybe_chunk) = req.body_mut().next().await {
match maybe_chunk {
Ok(chunk) => {
// Uncomment me!
// sleep(Duration::from_millis(100)).await;
let _ = tx.send(chunk.to_vec()).await;
}
Err(err) => {
println!("Got error: {}", err);
}
}
}
println!("finished reading body!");
});
// Create a response with a data stream that is filled by the reading task
let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
let response = Response::builder()
.body(Body::wrap_stream(stream.map(Result::<_, hyper::Error>::Ok)))
.expect("Failed to build response");
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = ([0, 0, 0, 0], 3000).into();
let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) });
let server = Server::bind(&addr).serve(service);
println!("Listening on http://{}", addr);
server.await?;
Ok(())
}
Cargo.toml dependencies:
[dependencies]
tokio = { version = "1", features = ["full"] }
hyper = { version="0.14", features=["full"] }
http = "*"
futures = "*"
futures-util = "0.3.28"
tokio-stream = "0.1.14"
Service above is an echo-server, which sends back the stream of data that it receives. To get the repro, please launch the service, and on another terminal start the "endless" curl command like this:
$ cat /dev/zero | curl -XPOST http://127.0.0.1:3000/ -H "Transfer-Encoding: chunked" -T - -o /dev/null
It will read /dev/zero
, pipe it to curl, which will send it as data stream with chunked transfer encoding, sending the output stream to /dev/null
.
After some time, terminate curl with Ctrl+C
. Service reports the error on the stream as expected, terminal output will look like this:
Listening on http://0.0.0.0:3000
reading body!
Got error: error reading a body from connection: Connection reset by peer (os error 104)
finished reading body!
But if we uncomment the sleep line, and repeat the experiment, there will be no error, as indicated by service output:
Listening on http://0.0.0.0:3000
reading body!
finished reading body!
I'm observing this weird behavior on a larger codebase that does approximately the same as sample server presented above.
One more interesting observation, even when sleep is in place, it does not repro (i.e. error is reported properly) with this netcat:
$ echo -ne "GET /echo-buffer HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n6\r\naabbcc" | nc localhos
t 3000; echo
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Fri, 16 Jun 2023 16:04:58 GMT
6
aabbcc
^C
My larger codebase also works as expected with netcat. Is there a race somewhere?