diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs index c922e6bf6822..981877fbcf44 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs @@ -51,7 +51,7 @@ internal class Http1OutputProducer : IHttpOutputProducer, IDisposable // Once write or flush is called, we modify the _currentChunkMemory to prepend the size of data written // and append the end terminator. - private bool _autoChunk; + private ResponseBodyMode _responseBodyMode; private bool _writeStreamSuffixCalled; @@ -121,7 +121,7 @@ public ValueTask WriteStreamSuffixAsync() { if (!_writeStreamSuffixCalled) { - if (_autoChunk) + if (_responseBodyMode == ResponseBodyMode.Chunked) { var writer = new BufferWriter(_pipeWriter); result = WriteAsyncInternal(ref writer, EndChunkedResponseBytes); @@ -147,7 +147,7 @@ public ValueTask FlushAsync(CancellationToken cancellationToken = d return new ValueTask(new FlushResult(false, true)); } - if (_autoChunk) + if (_responseBodyMode == ResponseBodyMode.Chunked) { if (_advancedBytesForChunk > 0) { @@ -173,7 +173,7 @@ static ValueTask FlushAsyncChunked(Http1OutputProducer producer, Ca // Local function so in the common-path the stack space for BufferWriter isn't reserved and cleared when it isn't used. Debug.Assert(!producer._pipeWriterCompleted); - Debug.Assert(producer._autoChunk && producer._advancedBytesForChunk > 0); + Debug.Assert(producer._responseBodyMode == ResponseBodyMode.Chunked && producer._advancedBytesForChunk > 0); var writer = new BufferWriter(producer._pipeWriter); producer.WriteCurrentChunkMemoryToPipeWriter(ref writer); @@ -203,7 +203,7 @@ public Memory GetMemory(int sizeHint = 0) { return LeasedMemory(sizeHint); } - else if (_autoChunk) + else if (_responseBodyMode == ResponseBodyMode.Chunked) { return GetChunkedMemory(sizeHint); } @@ -228,7 +228,7 @@ public Span GetSpan(int sizeHint = 0) { return LeasedMemory(sizeHint).Span; } - else if (_autoChunk) + else if (_responseBodyMode == ResponseBodyMode.Chunked) { return GetChunkedMemory(sizeHint).Span; } @@ -262,7 +262,7 @@ public void Advance(int bytes) _position += bytes; } } - else if (_autoChunk) + else if (_responseBodyMode == ResponseBodyMode.Chunked) { if (_advancedBytesForChunk > _currentChunkMemory.Length - _currentMemoryPrefixBytes - EndChunkLength - bytes) { @@ -333,7 +333,7 @@ private void CommitChunkInternal(ref BufferWriter writer, ReadOnlySp writer.Commit(); } - public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, bool appComplete) + public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, bool appComplete) { lock (_contextLock) { @@ -346,11 +346,11 @@ public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpRespo var buffer = _pipeWriter; var writer = new BufferWriter(buffer); - WriteResponseHeadersInternal(ref writer, statusCode, reasonPhrase, responseHeaders, autoChunk); + WriteResponseHeadersInternal(ref writer, statusCode, reasonPhrase, responseHeaders, responseBodyMode); } } - private void WriteResponseHeadersInternal(ref BufferWriter writer, int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk) + private void WriteResponseHeadersInternal(ref BufferWriter writer, int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode) { writer.Write(HttpVersion11Bytes); var statusBytes = ReasonPhrases.ToStatusBytes(statusCode, reasonPhrase); @@ -360,7 +360,8 @@ private void WriteResponseHeadersInternal(ref BufferWriter writer, i writer.Commit(); - _autoChunk = autoChunk; + Debug.Assert(responseBodyMode != ResponseBodyMode.Uninitialized); + _responseBodyMode = responseBodyMode; WriteDataWrittenBeforeHeaders(ref writer); _unflushedBytes += writer.BytesCommitted; @@ -373,11 +374,11 @@ private void WriteDataWrittenBeforeHeaders(ref BufferWriter writer) { foreach (var segment in _completedSegments) { - if (_autoChunk) + if (_responseBodyMode == ResponseBodyMode.Chunked) { CommitChunkInternal(ref writer, segment.Span); } - else + else if (_responseBodyMode == ResponseBodyMode.ContentLength) { writer.Write(segment.Span); writer.Commit(); @@ -391,16 +392,19 @@ private void WriteDataWrittenBeforeHeaders(ref BufferWriter writer) if (!_currentSegment.IsEmpty) { - var segment = _currentSegment.Slice(0, _position); - - if (_autoChunk) + if (_responseBodyMode != ResponseBodyMode.Disabled) { - CommitChunkInternal(ref writer, segment.Span); - } - else - { - writer.Write(segment.Span); - writer.Commit(); + var segment = _currentSegment.Slice(0, _position); + + if (_responseBodyMode == ResponseBodyMode.Chunked) + { + CommitChunkInternal(ref writer, segment.Span); + } + else if (_responseBodyMode == ResponseBodyMode.ContentLength) + { + writer.Write(segment.Span); + writer.Commit(); + } } _position = 0; @@ -491,7 +495,7 @@ public ValueTask Write100ContinueAsync() return WriteAsync(ContinueBytes); } - public ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan buffer, CancellationToken cancellationToken) + public ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan buffer, CancellationToken cancellationToken) { lock (_contextLock) { @@ -505,13 +509,13 @@ public ValueTask FirstWriteAsync(int statusCode, string? reasonPhra // Uses same BufferWriter to write response headers and response var writer = new BufferWriter(_pipeWriter); - WriteResponseHeadersInternal(ref writer, statusCode, reasonPhrase, responseHeaders, autoChunk); + WriteResponseHeadersInternal(ref writer, statusCode, reasonPhrase, responseHeaders, responseBodyMode); return WriteAsyncInternal(ref writer, buffer, cancellationToken); } } - public ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan buffer, CancellationToken cancellationToken) + public ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan buffer, CancellationToken cancellationToken) { lock (_contextLock) { @@ -525,7 +529,7 @@ public ValueTask FirstWriteChunkedAsync(int statusCode, string? rea // Uses same BufferWriter to write response headers and chunk var writer = new BufferWriter(_pipeWriter); - WriteResponseHeadersInternal(ref writer, statusCode, reasonPhrase, responseHeaders, autoChunk); + WriteResponseHeadersInternal(ref writer, statusCode, reasonPhrase, responseHeaders, responseBodyMode); CommitChunkInternal(ref writer, buffer); @@ -541,7 +545,7 @@ public void Reset() Debug.Assert(_completedSegments == null || _completedSegments.Count == 0); // Cleared in sequential address ascending order _currentMemoryPrefixBytes = 0; - _autoChunk = false; + _responseBodyMode = ResponseBodyMode.Uninitialized; _writeStreamSuffixCalled = false; _currentChunkMemoryUpdated = false; _startCalled = false; @@ -570,7 +574,7 @@ private ValueTask WriteAsyncInternal( ReadOnlySpan buffer, CancellationToken cancellationToken = default) { - if (_autoChunk) + if (_responseBodyMode == ResponseBodyMode.Chunked) { if (_advancedBytesForChunk > 0) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index eaae71743b57..6f5df3dd96f8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -54,12 +54,11 @@ internal abstract partial class HttpProtocol : IHttpResponseControl // Keep-alive is default for HTTP/1.1 and HTTP/2; parsing and errors will change its value // volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx protected volatile bool _keepAlive = true; - // _canWriteResponseBody is set in CreateResponseHeaders. + // _responseBodyMode is set in CreateResponseHeaders. // If we are writing with GetMemory/Advance before calling StartAsync, assume we can write and throw away contents if we can't. - private bool _canWriteResponseBody = true; + private ResponseBodyMode _responseBodyMode = ResponseBodyMode.Uninitialized; private bool _hasAdvanced; private bool _isLeasedMemoryInvalid = true; - private bool _autoChunk; protected Exception? _applicationException; private BadHttpRequestException? _requestRejectedException; @@ -347,7 +346,7 @@ public void Reset() _routeValues?.Clear(); _requestProcessingStatus = RequestProcessingStatus.RequestPending; - _autoChunk = false; + _responseBodyMode = ResponseBodyMode.Uninitialized; _applicationException = null; _requestRejectedException = null; @@ -397,7 +396,6 @@ public void Reset() _isLeasedMemoryInvalid = true; _hasAdvanced = false; - _canWriteResponseBody = true; if (_scheme == null) { @@ -999,7 +997,7 @@ private void ProduceStart(bool appCompleted) var responseHeaders = CreateResponseHeaders(appCompleted); - Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, appCompleted); + Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _responseBodyMode, appCompleted); } private void VerifyInitializeState(int firstWriteByteCount) @@ -1067,7 +1065,7 @@ protected Task ProduceEnd() private Task WriteSuffix() { - if (_autoChunk || _httpVersion >= Http.HttpVersion.Http2) + if (_responseBodyMode == ResponseBodyMode.Chunked || _httpVersion >= Http.HttpVersion.Http2) { // For the same reason we call CheckLastWrite() in Content-Length responses. PreventRequestAbortedCancellation(); @@ -1161,9 +1159,9 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) } // Set whether response can have body - _canWriteResponseBody = CanWriteResponseBody(); + _responseBodyMode = CanWriteResponseBody() ? ResponseBodyMode.ContentLength : ResponseBodyMode.Disabled; - if (!_canWriteResponseBody && hasTransferEncoding) + if (_responseBodyMode == ResponseBodyMode.Disabled && hasTransferEncoding) { RejectInvalidHeaderForNonBodyResponse(appCompleted, HeaderNames.TransferEncoding); } @@ -1197,7 +1195,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) } else if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue) { - if ((appCompleted || !_canWriteResponseBody) && !_hasAdvanced) // Avoid setting contentLength of 0 if we wrote data before calling CreateResponseHeaders + if ((appCompleted || _responseBodyMode == ResponseBodyMode.Disabled) && !_hasAdvanced) // Avoid setting contentLength of 0 if we wrote data before calling CreateResponseHeaders { if (CanAutoSetContentLengthZeroResponseHeader()) { @@ -1218,8 +1216,11 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) // The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2. else if (_httpVersion == Http.HttpVersion.Http11) { - _autoChunk = true; - responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked); + if (_responseBodyMode == ResponseBodyMode.ContentLength) + { + _responseBodyMode = ResponseBodyMode.Chunked; + responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked); + } } else { @@ -1470,7 +1471,7 @@ public void Advance(int bytes) throw new InvalidOperationException("Invalid ordering of calling StartAsync or CompleteAsync and Advance."); } - if (_canWriteResponseBody) + if (_responseBodyMode != ResponseBodyMode.Disabled) { VerifyAndUpdateWrite(bytes); Output.Advance(bytes); @@ -1591,28 +1592,28 @@ public ValueTask WritePipeAsync(ReadOnlyMemory data, Cancella VerifyAndUpdateWrite(data.Length); } - if (_canWriteResponseBody) + switch (_responseBodyMode) { - if (_autoChunk) - { + case ResponseBodyMode.Disabled: + HandleNonBodyResponseWrite(); + return default; + case ResponseBodyMode.Chunked: if (data.Length == 0) { return default; } return Output.WriteChunkAsync(data.Span, cancellationToken); - } - else - { + case ResponseBodyMode.ContentLength: CheckLastWrite(); return Output.WriteDataToPipeAsync(data.Span, cancellationToken: cancellationToken); - } - } - else - { - HandleNonBodyResponseWrite(); - return default; + case ResponseBodyMode.Uninitialized: + ThrowInvalidOperation(); + break; } + + Debug.Assert(false, "Should not reach here, all cases in above switch statement should return"); + return default; } private ValueTask FirstWriteAsync(ReadOnlyMemory data, CancellationToken cancellationToken) @@ -1639,30 +1640,30 @@ private ValueTask FirstWriteAsyncInternal(ReadOnlyMemory data { var responseHeaders = InitializeResponseFirstWrite(data.Length); - if (_canWriteResponseBody) + switch (_responseBodyMode) { - if (_autoChunk) - { + case ResponseBodyMode.Disabled: + Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _responseBodyMode, appCompleted: false); + HandleNonBodyResponseWrite(); + return Output.FlushAsync(cancellationToken); + case ResponseBodyMode.Chunked: if (data.Length == 0) { - Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, appCompleted: false); + Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _responseBodyMode, appCompleted: false); return Output.FlushAsync(cancellationToken); } - return Output.FirstWriteChunkedAsync(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, data.Span, cancellationToken); - } - else - { + return Output.FirstWriteChunkedAsync(StatusCode, ReasonPhrase, responseHeaders, _responseBodyMode, data.Span, cancellationToken); + case ResponseBodyMode.ContentLength: CheckLastWrite(); - return Output.FirstWriteAsync(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, data.Span, cancellationToken); - } - } - else - { - Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, appCompleted: false); - HandleNonBodyResponseWrite(); - return Output.FlushAsync(cancellationToken); + return Output.FirstWriteAsync(StatusCode, ReasonPhrase, responseHeaders, _responseBodyMode, data.Span, cancellationToken); + case ResponseBodyMode.Uninitialized: + ThrowInvalidOperation(); + break; } + + Debug.Assert(false, "Should not reach here, all cases in above switch statement should return"); + return default; } public Task FlushAsync(CancellationToken cancellationToken = default) @@ -1688,27 +1689,34 @@ public async ValueTask WriteAsyncAwaited(Task initializeTask, ReadO // WriteAsyncAwaited is only called for the first write to the body. // Ensure headers are flushed if Write(Chunked)Async isn't called. - if (_canWriteResponseBody) + switch (_responseBodyMode) { - if (_autoChunk) - { + case ResponseBodyMode.Disabled: + HandleNonBodyResponseWrite(); + return await Output.FlushAsync(cancellationToken); + case ResponseBodyMode.Chunked: if (data.Length == 0) { return await Output.FlushAsync(cancellationToken); } return await Output.WriteChunkAsync(data.Span, cancellationToken); - } - else - { + case ResponseBodyMode.ContentLength: CheckLastWrite(); return await Output.WriteDataToPipeAsync(data.Span, cancellationToken: cancellationToken); - } - } - else - { - HandleNonBodyResponseWrite(); - return await Output.FlushAsync(cancellationToken); + case ResponseBodyMode.Uninitialized: + ThrowInvalidOperation(); + break; } + + Debug.Assert(false, "Should not reach here, all cases in above switch statement should return"); + return default; + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowInvalidOperation() + { + throw new InvalidOperationException(); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputProducer.cs index d9733fc1c42d..87fe04f377d4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputProducer.cs @@ -13,7 +13,7 @@ internal interface IHttpOutputProducer ValueTask WriteChunkAsync(ReadOnlySpan data, CancellationToken cancellationToken); ValueTask FlushAsync(CancellationToken cancellationToken); ValueTask Write100ContinueAsync(); - void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, bool appCompleted); + void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, bool appCompleted); // This takes ReadOnlySpan instead of ReadOnlyMemory because it always synchronously copies data before flushing. ValueTask WriteDataToPipeAsync(ReadOnlySpan data, CancellationToken cancellationToken); // Test hook @@ -25,7 +25,15 @@ internal interface IHttpOutputProducer Memory GetMemory(int sizeHint = 0); void CancelPendingFlush(); void Stop(); - ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken); - ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken); + ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan data, CancellationToken cancellationToken); + ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan data, CancellationToken cancellationToken); void Reset(); } + +internal enum ResponseBodyMode +{ + Uninitialized, + Disabled, + Chunked, + ContentLength +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index 1a1e6a3a9879..d65a6b9e0bf0 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -358,7 +358,7 @@ public ValueTask Write100ContinueAsync() } } - public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, bool appCompleted) + public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, bool appCompleted) { lock (_dataWriterLock) { @@ -552,11 +552,11 @@ public ValueTask WriteDataToPipeAsync(ReadOnlySpan data, Canc } } - public ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken) + public ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan data, CancellationToken cancellationToken) { lock (_dataWriterLock) { - WriteResponseHeaders(statusCode, reasonPhrase, responseHeaders, autoChunk, appCompleted: false); + WriteResponseHeaders(statusCode, reasonPhrase, responseHeaders, responseBodyMode, appCompleted: false); return WriteDataToPipeAsync(data, cancellationToken); } @@ -567,7 +567,7 @@ ValueTask IHttpOutputProducer.WriteChunkAsync(ReadOnlySpan da throw new NotImplementedException(); } - public ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken) + public ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan data, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs index 94b5e0f094bf..bc444d0b0dc6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs @@ -149,17 +149,17 @@ public void CancelPendingFlush() } } - public ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken) + public ValueTask FirstWriteAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan data, CancellationToken cancellationToken) { lock (_dataWriterLock) { - WriteResponseHeaders(statusCode, reasonPhrase, responseHeaders, autoChunk, appCompleted: false); + WriteResponseHeaders(statusCode, reasonPhrase, responseHeaders, responseBodyMode, appCompleted: false); return WriteDataToPipeAsync(data, cancellationToken); } } - public ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken) + public ValueTask FirstWriteChunkedAsync(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, ReadOnlySpan data, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -375,7 +375,7 @@ public ValueTask WriteDataToPipeAsync(ReadOnlySpan data, Canc } } - public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, bool appCompleted) + public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpResponseHeaders responseHeaders, ResponseBodyMode responseBodyMode, bool appCompleted) { // appCompleted flag is not used here. The write FIN is sent via the transport and not via the frame. // Headers are written to buffer and flushed with a FIN when Http3FrameWriter.CompleteAsync is called diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs index 8d5c6e65e0c3..22624e266216 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs @@ -1426,12 +1426,15 @@ await connection.Receive( $"Date: {server.Context.DateHeaderValue}", "", ""); + + connection.ShutdownSend(); + await connection.ReceiveEnd(); } } } [Fact] - public async Task HeadResponseBodyNotWrittenWithAsyncWrite() + public async Task HeadResponseHeadersWrittenWithAsyncWriteBeforeAppCompletes() { var flushed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -1461,8 +1464,68 @@ await connection.Receive( } } + [Fact] + public async Task HeadResponseBodyNotWrittenWithAsyncWrite() + { + await using (var server = new TestServer(async httpContext => + { + httpContext.Response.ContentLength = 12; + await httpContext.Response.WriteAsync("hello, world"); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "HEAD / HTTP/1.1", + "Host:", + "", + ""); + await connection.Receive( + "HTTP/1.1 200 OK", + "Content-Length: 12", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + + connection.ShutdownSend(); + await connection.ReceiveEnd(); + } + } + } + [Fact] public async Task HeadResponseBodyNotWrittenWithSyncWrite() + { + var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } }; + + await using (var server = new TestServer(httpContext => + { + httpContext.Response.ContentLength = 12; + httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12); + return Task.CompletedTask; + }, serviceContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "HEAD / HTTP/1.1", + "Host:", + "", + ""); + await connection.Receive( + "HTTP/1.1 200 OK", + "Content-Length: 12", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + connection.ShutdownSend(); + await connection.ReceiveEnd(); + } + } + } + + [Fact] + public async Task HeadResponseHeadersWrittenWithSyncWriteBeforeAppCompletes() { var flushed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -1471,7 +1534,7 @@ public async Task HeadResponseBodyNotWrittenWithSyncWrite() await using (var server = new TestServer(async httpContext => { httpContext.Response.ContentLength = 12; - await httpContext.Response.BodyWriter.WriteAsync(new Memory(Encoding.ASCII.GetBytes("hello, world"), 0, 12)); + httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12); await flushed.Task; }, serviceContext)) { @@ -1494,6 +1557,131 @@ await connection.Receive( } } + [Fact] + public async Task HeadResponseBodyNotWrittenWithAdvanceBeforeFlush() + { + var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } }; + + await using (var server = new TestServer(async httpContext => + { + var span = httpContext.Response.BodyWriter.GetSpan(5); + for (var i = 0; i < span.Length; i++) + { + span[i] = (byte)'h'; + } + httpContext.Response.BodyWriter.Advance(span.Length); + await httpContext.Response.BodyWriter.FlushAsync(); + }, serviceContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "HEAD / HTTP/1.1", + "Host:", + "", + ""); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + + connection.ShutdownSend(); + await connection.ReceiveEnd(); + } + } + } + + // Rough attempt at checking that a non-body response doesn't affect future body responses + [Fact] + public async Task GetRequestAfterHeadRequestWorks() + { + var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } }; + + await using (var server = new TestServer(async httpContext => + { + await httpContext.Response.BodyWriter.WriteAsync(new byte[] { 35, 35, 35 }); + }, serviceContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "HEAD / HTTP/1.1", + "Host:", + "", + ""); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + + await connection.Send( + "GET /a HTTP/1.1", + "Host:", + "", + ""); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "3", + "###", + "0", + "", + ""); + connection.ShutdownSend(); + await connection.ReceiveEnd(); + } + } + } + + [Fact] + public async Task HeadResponseBodyNotWrittenWithAdvanceBeforeAndAfterFlush() + { + var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } }; + + await using (var server = new TestServer(async httpContext => + { + // Make response chunked + var span = httpContext.Response.BodyWriter.GetSpan(5); + for (var i = 0; i < span.Length; i++) + { + span[i] = (byte)'h'; + } + httpContext.Response.BodyWriter.Advance(span.Length); + await httpContext.Response.BodyWriter.FlushAsync(); + + // Send after headers flushed + span = httpContext.Response.BodyWriter.GetSpan(5); + for (var i = 0; i < span.Length; i++) + { + span[i] = (byte)'h'; + } + httpContext.Response.BodyWriter.Advance(span.Length); + await httpContext.Response.BodyWriter.FlushAsync(); + }, serviceContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "HEAD / HTTP/1.1", + "Host:", + "", + ""); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + + connection.ShutdownSend(); + await connection.ReceiveEnd(); + } + } + } + [Fact] public async Task ZeroLengthWritesFlushHeaders() {