@@ -65,10 +65,21 @@ private class DuplexStream(IDuplexPipe duplexPipe, CancellationTokenSource conne
6565 public override bool CanWrite => true ;
6666 public override bool CanSeek => false ;
6767
68- public override Task < int > ReadAsync ( byte [ ] buffer , int offset , int count , CancellationToken cancellationToken )
69- => _readStream . ReadAsync ( buffer , offset , count , cancellationToken ) ;
70- public override ValueTask < int > ReadAsync ( Memory < byte > buffer , CancellationToken cancellationToken = default )
71- => _readStream . ReadAsync ( buffer , cancellationToken ) ;
68+ public override async Task < int > ReadAsync ( byte [ ] buffer , int offset , int count , CancellationToken cancellationToken )
69+ {
70+ // Normally, Kestrel will trigger RequestAborted when the connectionClosedCts fires causing it to gracefully close
71+ // the connection. However, there's currently a race condition that can cause this to get missed. This at least
72+ // unblocks HttpConnection.SendAsync when it disposes the underlying connection stream while awaiting the _readAheadTask
73+ // as would happen with a real socket. https://github.com/dotnet/aspnetcore/pull/62385
74+ using var linkedTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , connectionClosedCts . Token ) ;
75+ return await _readStream . ReadAsync ( buffer , offset , count , linkedTokenSource . Token ) ;
76+ }
77+
78+ public override async ValueTask < int > ReadAsync ( Memory < byte > buffer , CancellationToken cancellationToken = default )
79+ {
80+ using var linkedTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , connectionClosedCts . Token ) ;
81+ return await _readStream . ReadAsync ( buffer , linkedTokenSource . Token ) ;
82+ }
7283
7384 public override Task WriteAsync ( byte [ ] buffer , int offset , int count , CancellationToken cancellationToken )
7485 => _writeStream . WriteAsync ( buffer , offset , count , cancellationToken ) ;
0 commit comments