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 6958e309f55ec..01640d1369290 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 @@ -63,6 +63,7 @@ internal sealed partial class HttpConnection : HttpConnectionBase, IDisposable private bool _inUse; private bool _canRetry; + private bool _startedSendingRequestBody; private bool _connectionClose; // Connection: close was seen on last response private const int Status_Disposed = 1; @@ -385,8 +386,8 @@ public async Task SendAsyncCore(HttpRequestMessage request, _currentRequest = request; HttpMethod normalizedMethod = HttpMethod.Normalize(request.Method); - Debug.Assert(!_canRetry); - _canRetry = true; + _canRetry = false; + _startedSendingRequestBody = false; // Send the request. if (NetEventSource.Log.IsEnabled()) Trace($"Sending request: {request}"); @@ -561,22 +562,28 @@ public async Task SendAsyncCore(HttpRequestMessage request, if (NetEventSource.Log.IsEnabled()) Trace($"Received {bytesRead} bytes."); - if (bytesRead == 0) - { - throw new IOException(SR.net_http_invalid_response_premature_eof); - } - _readOffset = 0; _readLength = bytesRead; } else { - await FillAsync(async).ConfigureAwait(false); + // No read-ahead, so issue a read ourselves. We will check below for EOF. + await InitialFillAsync(async).ConfigureAwait(false); + } + + if (_readLength == 0) + { + // The server shutdown the connection on their end, likely because of an idle timeout. + // If we haven't started sending the request body yet (or there is no request body), + // then we allow the request to be retried. + if (!_startedSendingRequestBody) + { + _canRetry = true; + } + + throw new IOException(SR.net_http_invalid_response_premature_eof); } - // We have received data from the server, so the request is no longer retryable. - // (We may have already set this above if we sent request content.) - _canRetry = false; // Parse the response status line. var response = new HttpResponseMessage() { RequestMessage = request, Content = new HttpConnectionResponseContent() }; @@ -866,8 +873,8 @@ private CancellationTokenRegistration RegisterCancellation(CancellationToken can private async ValueTask SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, bool async, CancellationToken cancellationToken) { - // Now that we're sending content, prohibit retries on this connection. - _canRetry = false; + // Now that we're sending content, prohibit retries of this request by setting this flag. + _startedSendingRequestBody = true; Debug.Assert(stream.BytesWritten == 0); if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestContentStart(); @@ -1553,6 +1560,19 @@ private void Fill() fillTask.GetAwaiter().GetResult(); } + // Does not throw on EOF. Also assumes there is no buffered data. + private async ValueTask InitialFillAsync(bool async) + { + Debug.Assert(_readAheadTask == null); + + _readOffset = 0; + _readLength = async ? + await _stream.ReadAsync(_readBuffer).ConfigureAwait(false) : + _stream.Read(_readBuffer); + + if (NetEventSource.Log.IsEnabled()) Trace($"Received {_readLength} bytes."); + } + // Throws IOException on EOF. This is only called when we expect more data. private async ValueTask FillAsync(bool async) {