Skip to content

Automatically disabled keep-alive destroys hop-by-hop headers #4195

Description

@dtolnay

Version

hyper 1.11.1

Platform

all

Summary

Since hyper 1.11.1, custom hop-by-hop connection headers from the client no longer make it to the server.

For example if the client sends this:

connection: close, x-hop
x-hop: ...

In hyper 1.11.0 and older it used to arrive as written. In 1.11.1 it arrives as:

connection: close
x-hop: ...

This bisects to #4110 which causes headers.insert(CONNECTION, "close") to be called in enforce_version. The behavior of http::header::HeaderMap::insert is:

If the map did have this key present, the new value is associated with the key and all previous values are removed.

Code Sample

// [dependencies]
// anyhow = "1"
// bytes = "1"
// http = "1"
// http-body-util = "0.1"
// hyper = { version = "1", features = ["client", "http1", "server"] }
// hyper-util = { version = "0.1", features = ["tokio"] }
// tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }

use bytes::Bytes;
use http::{Request, Response};
use http_body_util::{BodyExt, Empty};
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
use std::net::Ipv4Addr;
use tokio::net::{TcpListener, TcpStream};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
    let addr = listener.local_addr()?;

    let server = tokio::spawn(async move {
        let (stream, _) = listener.accept().await?;
        hyper::server::conn::http1::Builder::new()
            .keep_alive(true)
            .serve_connection(
                TokioIo::new(stream),
                service_fn(async |req| {
                    for (header, value) in req.headers() {
                        println!("{header}: {value:?}");
                    }
                    anyhow::Ok(Response::new(Empty::<Bytes>::new()))
                }),
            )
            .await?;
        anyhow::Ok(())
    });

    let stream = TcpStream::connect(addr).await?;
    let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
    tokio::spawn(conn);

    let req = Request::builder()
        .uri("/")
        .header(http::header::CONNECTION, "close, x-hop")
        .header(http::header::CONNECTION, "y-hop")
        .header("x-hop", "...")
        .header("y-hop", "...")
        .body(Empty::<Bytes>::new())?;

    let res = sender.send_request(req).await?;
    let mut body = res.into_body();
    while body.frame().await.is_some() {}
    drop(sender);
    server.await??;
    Ok(())
}

Expected Behavior

This program starts a server on localhost and sends it a request containing custom hop-by-hop headers. The server prints the headers received.

connection: "close, x-hop"
connection: "y-hop"
x-hop: "..."
y-hop: "..."

Actual Behavior

Because of the regression, "x-hop" and "y-hop" are missing from the connection header.

connection: "close"
x-hop: "..."
y-hop: "..."

Additional Context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: bug. Something is wrong. This is bad!

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions