diff --git a/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs b/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs index 0e02daa33063..55a98f4eeebd 100644 --- a/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs +++ b/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs @@ -20,6 +20,9 @@ public interface IHttpRequestBodyDetectionFeature /// /// It's an HTTP/2 request that did not set the END_STREAM flag on the initial headers frame. /// + /// + /// It's an HTTP/3 request that did not set the END_STREAM flag on the initial headers frame. + /// /// /// The final request body length may still be zero for the chunked or HTTP/2 scenarios. /// @@ -35,6 +38,9 @@ public interface IHttpRequestBodyDetectionFeature /// /// It's an HTTP/2 request that set END_STREAM on the initial headers frame. /// + /// + /// It's an HTTP/3 request that set END_STREAM on the initial headers frame. + /// /// /// /// When false, the request body should never return data. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 36c87cd8ef7b..672c566b7538 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -54,6 +54,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS private bool _isMethodConnect; private bool _isWebTransportSessionAccepted; private Http3MessageBody? _messageBody; + private bool _requestBodyStarted; private readonly ManualResetValueTaskSource _appCompletedTaskSource = new(); private readonly Lock _completionLock = new(); @@ -65,6 +66,17 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS private bool IsAbortedRead => (_completionState & StreamCompletionFlags.AbortedRead) == StreamCompletionFlags.AbortedRead; public bool IsCompleted => (_completionState & StreamCompletionFlags.Completed) == StreamCompletionFlags.Completed; + public bool ReceivedEmptyRequestBody + { + get + { + lock (_completionLock) + { + return EndStreamReceived && !_requestBodyStarted; + } + } + } + public Pipe RequestBodyPipe { get; private set; } = default!; public long? InputRemaining { get; internal set; } public QPackDecoder QPackDecoder { get; private set; } = default!; @@ -560,6 +572,8 @@ private void CompleteStream(bool errored) TryClose(); } + RequestBodyPipe.Reader.Complete(); + _http3Output.Complete(); // Stream will be pooled after app completed. @@ -929,6 +943,8 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) return Task.CompletedTask; } + _requestBodyStarted = true; + foreach (var segment in payload) { RequestBodyPipe.Writer.Write(segment.Span); @@ -966,6 +982,11 @@ protected override string CreateRequestId() protected override MessageBody CreateMessageBody() { + if (ReceivedEmptyRequestBody) + { + return MessageBody.ZeroContentLengthClose; + } + if (_messageBody != null) { _messageBody.Reset(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index b7697117452e..0d5e22d295a4 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -3291,6 +3291,32 @@ public async Task Control_FrameParsingSingleByteAtATimeWorks() await outboundcontrolStream.ReceiveEndAsync(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CanHaveBody_ReturnsFalseWithoutRequestBody(bool endstream) + { + var headers = new[] + { + new KeyValuePair(InternalHeaderNames.Method, "GET"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + new KeyValuePair(InternalHeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => + { + Assert.NotEqual(endstream, context.Features.Get().CanHaveBody); + context.Response.StatusCode = 200; + return Task.CompletedTask; + }, headers, endStream: endstream); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + Assert.Equal("200", responseHeaders[InternalHeaderNames.Status]); + + await requestStream.ExpectReceiveEndOfStream(); + } + private async Task WriteOneByteAtATime(PipeReader reader, PipeWriter writer) { var res = await reader.ReadAsync();