hyperium / hyperium/hyper

HTTP2 requests waiting an outstanding connecting task should not be canceled when the connecting task is canceled

Open
#3,199 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-bug
Dominant language
Rust
Stars
16.3k
Forks
1.8k
Avg merge
1d 22h
Merged PRs (30d)
14

Description

Version
0.14.25. I guess it applies also to the pool in hyper-util, but haven't really checked.

Platform
Darwin Kernel Version 21.4.0: Mon Feb 21 20:35:58 PST 2022; root:xnu-8020.101.4~2/RELEASE_ARM64_T6000 arm64

Description

When there are a bunch of HTTP2 requests waiting for a connection from the pool and there is already a connecting task, if the connecting task is dropped before the waiters receive the new connection we drop all the waiter senders, resulting in the cancelation of the awaiting requests, arguing that the connecting attempt failed. However, this also happens if the connecting attempt is cancelled by the user (ie the ResponseFuture is dropped) and not technically failed.

I've written this test that reproduces (what I consider) the bug:

    #[tokio::test]
    async fn http2_connection_waiters_are_not_canceled_when_outstanding_connecting_task_is_canceled() {
        let _ = pretty_env_logger::try_init();

        let server = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = server.local_addr().unwrap();

        #[derive(Clone)]
        struct SlowConnector;

        impl hyper::service::Service<Uri> for SlowConnector {
            type Response = TcpStream;
            type Error = hyper::Error;
            type Future = future::Pending<Result<TcpStream, hyper::Error>>;

            fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
                Poll::Ready(Ok(()))
            }

            fn call(&mut self, _dst: Uri) -> Self::Future {
                // An slow connector...
                future::pending()
            }
        }

        let client = Client::builder().http2_only(true).build(SlowConnector);

        // This first request starts the connecting task 
        let req = Request::builder()
            .uri(&*format!("http://{}/a", addr))
            .body(Body::empty())
            .unwrap();
        let mut res1 = client.request(req).map(|_| unreachable!());

        // This second request waits for the conencting task to finish
        // instead of starting a new connecting task
        let req = Request::builder()
            .uri(&*format!("http://{}/a", addr))
            .body(Body::empty())
            .unwrap();
        let mut res2 = client
            .request(req)
            // Test fails here with
            // thread 'dispatch_impl::http2_connection_waiters_are_not_canceled_when_outstanding_connecting_task_is_canceled'
            // panicked at 'internal error: entered unreachable code: res2 should had never resolved, but resulted in
            // Err(hyper::Error(Canceled, "request has been canceled"))', tests/client.rs:1398:22
            .map(|r| unreachable!("res2 should had never resolved, but resulted in {:?}", r));

        // Prime the requests
        assert!(
            future::poll_fn(|ctx| Poll::Ready(Pin::new(&mut res1).poll(ctx).is_pending())).await
        );
        assert!(
            future::poll_fn(|ctx| Poll::Ready(Pin::new(&mut res2).poll(ctx).is_pending())).await
        );

        // The `ResponseFuture` that drives the connecting task is dropped, and the connecting task is canceled
        drop(res1);

        // The request that was waiting for the connecting task fails with Error::Canceled
        assert!(
            future::poll_fn(|ctx| Poll::Ready(Pin::new(&mut res2).poll(ctx).is_pending())).await
        );
    }

Thoughts? I'm up to work on a solution if we agree on the assessment.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/client/pool.rs at the cited waiter-sender and connecting-task handling around lines 397-400 and 706-715, then review the reproducer in tests/client.rs. Run the HTTP/2 waiter test and verify that dropping the first ResponseFuture does not cancel the second request while it is waiting for the connection.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.