Skip to content

Commit a419089

Browse files
committed
fix(client): race in connection errors propagation
Fix a race condition in the legacy HTTP client's connection setup where connection errors (e.g., TLS failures, unexpected server responses) were discarded, resulting in vague ChannelClosed errors. seanmonstar/reqwest#2649
1 parent 7e74248 commit a419089

File tree

1 file changed

+72
-5
lines changed

1 file changed

+72
-5
lines changed

src/client/legacy/client.rs

+72-5
Original file line numberDiff line numberDiff line change
@@ -576,22 +576,89 @@ where
576576
panic!("http2 feature is not enabled");
577577
} else {
578578
#[cfg(feature = "http1")] {
579+
// Perform the HTTP/1.1 handshake on the provided I/O stream.
580+
// Uses the h1_builder to establish a connection, returning a sender (tx) for requests
581+
// and a connection task (conn) that manages the connection lifecycle.
579582
let (mut tx, conn) =
580-
h1_builder.handshake(io).await.map_err(Error::tx)?;
583+
h1_builder.handshake(io).await.map_err(crate::client::legacy::client::Error::tx)?;
581584

585+
// Log that the HTTP/1.1 handshake has completed successfully.
586+
// This indicates the connection is established and ready for request processing.
582587
trace!(
583588
"http1 handshake complete, spawning background dispatcher task"
584589
);
590+
// Create a oneshot channel to communicate errors from the connection task.
591+
// err_tx sends errors from the connection task, and err_rx receives them
592+
// to correlate connection failures with request readiness errors.
593+
let (err_tx, err_rx) = tokio::sync::oneshot::channel();
594+
// Spawn the connection task in the background using the executor.
595+
// The task manages the HTTP/1.1 connection, including upgrades (e.g., WebSocket).
596+
// Errors are sent via err_tx to ensure they can be checked if the sender (tx) fails.
585597
executor.execute(
586598
conn.with_upgrades()
587-
.map_err(|e| debug!("client connection error: {}", e))
599+
.map_err(|e| {
600+
// Log the connection error at debug level for diagnostic purposes.
601+
debug!("client connection error: {:?}", e);
602+
// Log that the error is being sent to the error channel.
603+
trace!("sending connection error to error channel");
604+
// Send the error via the oneshot channel, ignoring send failures
605+
// (e.g., if the receiver is dropped, which is handled later).
606+
let _ =err_tx.send(e);
607+
})
588608
.map(|_| ()),
589609
);
590-
610+
// Log that the client is waiting for the connection to be ready.
611+
// Readiness indicates the sender (tx) can accept a request without blocking.
612+
trace!("waiting for connection to be ready");
613+
// Check if the sender is ready to accept a request.
614+
// This ensures the connection is fully established before proceeding.
615+
// aka:
591616
// Wait for 'conn' to ready up before we
592617
// declare this tx as usable
593-
tx.ready().await.map_err(Error::tx)?;
594-
PoolTx::Http1(tx)
618+
match tx.ready().await {
619+
// If ready, the connection is usable for sending requests.
620+
Ok(_) => {
621+
// Log that the connection is ready for use.
622+
trace!("connection is ready");
623+
// Drop the error receiver, as it’s no longer needed since the sender is ready.
624+
// This prevents waiting for errors that won’t occur in a successful case.
625+
drop(err_rx);
626+
// Wrap the sender in PoolTx::Http1 for use in the connection pool.
627+
PoolTx::Http1(tx)
628+
}
629+
// If the sender fails with a closed channel error, check for a specific connection error.
630+
// This distinguishes between a vague ChannelClosed error and an actual connection failure.
631+
Err(e) if e.is_closed() => {
632+
// Log that the channel is closed, indicating a potential connection issue.
633+
trace!("connection channel closed, checking for connection error");
634+
// Check the oneshot channel for a specific error from the connection task.
635+
match err_rx.await {
636+
// If an error was received, it’s a specific connection failure.
637+
Ok(err) => {
638+
// Log the specific connection error for diagnostics.
639+
trace!("received connection error: {:?}", err);
640+
// Return the error wrapped in Error::tx to propagate it.
641+
return Err(crate::client::legacy::client::Error::tx(err));
642+
}
643+
// If the error channel is closed, no specific error was sent.
644+
// Fall back to the vague ChannelClosed error.
645+
Err(_) => {
646+
// Log that the error channel is closed, indicating no specific error.
647+
trace!("error channel closed, returning the vague ChannelClosed error");
648+
// Return the original error wrapped in Error::tx.
649+
return Err(crate::client::legacy::client::Error::tx(e));
650+
}
651+
}
652+
}
653+
// For other errors (e.g., timeout, I/O issues), propagate them directly.
654+
// These are not ChannelClosed errors and don’t require error channel checks.
655+
Err(e) => {
656+
// Log the specific readiness failure for diagnostics.
657+
trace!("connection readiness failed: {:?}", e);
658+
// Return the error wrapped in Error::tx to propagate it.
659+
return Err(crate::client::legacy::client::Error::tx(e));
660+
}
661+
}
595662
}
596663
#[cfg(not(feature = "http1"))] {
597664
panic!("http1 feature is not enabled");

0 commit comments

Comments
 (0)