Skip to content

Commit ff0e49c

Browse files
author
ladeak
committed
Enhance HTTP/3 request body detection and testing
- Updated `IHttpRequestBodyDetectionFeature` to include HTTP/3 descriptions for the `END_STREAM` flag. - Modified request body handling logic to return zero length content body instance when there endstream flag is set. It also completes the message body for empty requests, so that the RequestBodyPipe.Reader is closed (needed when stream is reused by the pool and the Pipe is reset). - Added unit test `CanHaveBody_ReturnsFalseWithoutRequestBody` to validate behavior when no request body is present.
1 parent 4fccb63 commit ff0e49c

File tree

3 files changed

+51
-1
lines changed

3 files changed

+51
-1
lines changed

src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public interface IHttpRequestBodyDetectionFeature
2020
/// <item><description>
2121
/// It's an HTTP/2 request that did not set the END_STREAM flag on the initial headers frame.
2222
/// </description></item>
23+
/// <item><description>
24+
/// It's an HTTP/3 request that did not set the END_STREAM flag on the initial headers frame.
25+
/// </description></item>
2326
/// </list>
2427
/// The final request body length may still be zero for the chunked or HTTP/2 scenarios.
2528
/// <para>
@@ -35,6 +38,9 @@ public interface IHttpRequestBodyDetectionFeature
3538
/// <item><description>
3639
/// It's an HTTP/2 request that set END_STREAM on the initial headers frame.
3740
/// </description></item>
41+
/// <item><description>
42+
/// It's an HTTP/3 request that set END_STREAM on the initial headers frame.
43+
/// </description></item>
3844
/// </list>
3945
/// </para>
4046
/// When false, the request body should never return data.

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
5454
private bool _isMethodConnect;
5555
private bool _isWebTransportSessionAccepted;
5656
private Http3MessageBody? _messageBody;
57+
private bool _requestBodyStarted;
5758

5859
private readonly ManualResetValueTaskSource<object?> _appCompletedTaskSource = new();
5960
private readonly Lock _completionLock = new();
@@ -65,6 +66,17 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
6566
private bool IsAbortedRead => (_completionState & StreamCompletionFlags.AbortedRead) == StreamCompletionFlags.AbortedRead;
6667
public bool IsCompleted => (_completionState & StreamCompletionFlags.Completed) == StreamCompletionFlags.Completed;
6768

69+
public bool ReceivedEmptyRequestBody
70+
{
71+
get
72+
{
73+
lock (_completionLock)
74+
{
75+
return EndStreamReceived && !_requestBodyStarted;
76+
}
77+
}
78+
}
79+
6880
public Pipe RequestBodyPipe { get; private set; } = default!;
6981
public long? InputRemaining { get; internal set; }
7082
public QPackDecoder QPackDecoder { get; private set; } = default!;
@@ -928,7 +940,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence<byte> payload)
928940
{
929941
return Task.CompletedTask;
930942
}
931-
943+
_requestBodyStarted = true;
932944
foreach (var segment in payload)
933945
{
934946
RequestBodyPipe.Writer.Write(segment.Span);
@@ -975,6 +987,12 @@ protected override MessageBody CreateMessageBody()
975987
_messageBody = new Http3MessageBody(this);
976988
}
977989

990+
if (ReceivedEmptyRequestBody)
991+
{
992+
_messageBody.Complete(exception: null);
993+
return MessageBody.ZeroContentLengthClose;
994+
}
995+
978996
return _messageBody;
979997
}
980998

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3291,6 +3291,32 @@ public async Task Control_FrameParsingSingleByteAtATimeWorks()
32913291
await outboundcontrolStream.ReceiveEndAsync();
32923292
}
32933293

3294+
[Theory]
3295+
[InlineData(true)]
3296+
[InlineData(false)]
3297+
public async Task CanHaveBody_ReturnsFalseWithoutRequestBody(bool endstream)
3298+
{
3299+
var headers = new[]
3300+
{
3301+
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
3302+
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
3303+
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
3304+
new KeyValuePair<string, string>(InternalHeaderNames.Authority, "localhost:80"),
3305+
};
3306+
3307+
var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context =>
3308+
{
3309+
Assert.Equal(!endstream, context.Features.Get<IHttpRequestBodyDetectionFeature>().CanHaveBody);
3310+
context.Response.StatusCode = 200;
3311+
return Task.CompletedTask;
3312+
}, headers, endStream: endstream);
3313+
3314+
var responseHeaders = await requestStream.ExpectHeadersAsync();
3315+
Assert.Equal("200", responseHeaders[InternalHeaderNames.Status]);
3316+
3317+
await requestStream.ExpectReceiveEndOfStream();
3318+
}
3319+
32943320
private async Task WriteOneByteAtATime(PipeReader reader, PipeWriter writer)
32953321
{
32963322
var res = await reader.ReadAsync();

0 commit comments

Comments
 (0)