diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index 0b03489d93aff3..e2b49083df744d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -2056,9 +2056,14 @@ private void CompleteResponse() _connectionClose = true; } - // If the connection is no longer in use (i.e. for NT authentication), then we can return it to the pool now. - // Otherwise, it will be returned when the connection is no longer in use (i.e. Release above is called). - if (!_inUse) + // If the connection is no longer in use (i.e. for NT authentication), then we can + // return it to the pool now; otherwise, it will be returned by the Release method later. + // The cancellation logic in HTTP/1.1 response stream reading methods is prone to race conditions + // where CancellationTokenRegistration callbacks may dispose the connection without the disposal + // leading to an actual cancellation of the response reading methods by an OperationCanceledException. + // To guard against these cases, it is necessary to check if the connection is disposed before + // attempting to return it to the pool. + if (!_inUse && !_disposed) { ReturnConnectionToPool(); } @@ -2097,6 +2102,7 @@ public async ValueTask DrainResponseAsync(HttpResponseMessage response, Cancella private void ReturnConnectionToPool() { + Debug.Assert(!_disposed, "Connection should not be disposed."); Debug.Assert(_currentRequest == null, "Connection should no longer be associated with a request."); Debug.Assert(_readAheadTask == default, "Expected a previous initial read to already be consumed."); Debug.Assert(_readAheadTaskStatus == ReadAheadTask_NotStarted, "Expected SendAsync to reset the read-ahead task status.");