Skip to content

Missing error when reading stream of data out of an interrupted chunked-encoded request #3253

Closed
@Lupus

Description

@Lupus

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-http1Area: HTTP/1 specific.C-bugCategory: bug. Something is wrong. This is bad!E-mediumEffort: medium. Some knowledge of how hyper internal works would be useful.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions