Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/proto/h2/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,27 @@ where
Ok(()) => (),
Err(err) => {
self.ping.ensure_not_timed_out()?;
return if err.reason() == Some(::h2::Reason::NO_ERROR) {
let graceful = err.reason() == Some(::h2::Reason::NO_ERROR);
// A request may be parked in fut_ctx waiting for its
// stream to open. Fail it here with the connection
// error instead of letting it fall through to
// Callback::drop, which reports a misleading
// DispatchGone user error.
if let Some(f) = self.fut_ctx.take() {
let cb_err = if graceful {
crate::Error::new_canceled().with("connection closed")
} else {
match err.reason() {
Some(reason) => crate::Error::new_h2(reason.into()),
None => crate::Error::new_canceled().with("connection closed"),
}
};
f.cb.send(Err(TrySendError {
error: cb_err,
message: None,
}));
}
return if graceful {
trace!("connection gracefully shutdown");
Poll::Ready(Ok(Dispatched::Shutdown))
} else {
Expand Down
62 changes: 62 additions & 0 deletions tests/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2971,6 +2971,68 @@ mod conn {
upgraded.shutdown().await.unwrap();
}

#[tokio::test]
async fn h2_pending_open_fails_with_connection_error_on_close() {
// A request parked waiting for a stream slot (the server only
// allows one concurrent stream and the first is still open) must
// fail with a connection error when the peer goes away, not with
// a misleading DispatchGone user error.
let (client_io, server_io, _) = setup_duplex_test_server();

tokio::spawn(async move {
let sock = server_io;
let mut h2 = h2::server::Builder::new()
.max_concurrent_streams(1)
.handshake::<_, bytes::Bytes>(sock)
.await
.unwrap();

// Accept the first request and hold its stream open, so the
// second request parks waiting for a stream slot.
let (_req, respond) = h2.accept().await.unwrap().unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
// Close the connection while the second request is parked.
drop(respond);
drop(h2);
});

let io = TokioIo::new(client_io);
let (mut client, conn) = conn::http2::Builder::new(TokioExecutor)
.handshake(io)
.await
.expect("http handshake");

tokio::spawn(async move {
let _ = conn.await;
});

let mut client2 = client.clone();
let req1 = Request::builder()
.uri("http://localhost/")
.body(Empty::<Bytes>::new())
.unwrap();
let res1 = tokio::spawn(async move { client.send_request(req1).await });
let req2 = Request::builder()
.uri("http://localhost/")
.body(Empty::<Bytes>::new())
.unwrap();
let res2 = tokio::spawn(async move { client2.send_request(req2).await });

// The first request stays open on the server, the second parks.
// Both fail once the connection closes; the parked one must not
// come back as a user (DispatchGone) error.
let err = res2.await.unwrap().expect_err("parked request must fail");
assert!(
!err.is_user(),
"parked request failed with a user error instead of a connection error: {err:?}"
);
assert!(
err.is_canceled(),
"parked request should fail as canceled when the connection closes: {err:?}"
);
let _ = res1.await;
}

#[tokio::test]
async fn h2_connect_rejected() {
let (client_io, server_io, _) = setup_duplex_test_server();
Expand Down
Loading