Skip to content

Commit 3932156

Browse files
authored
Improve Http1ContentLengthMessageBody's reset logic (#25799)
1 parent a1276de commit 3932156

File tree

3 files changed

+103
-23
lines changed

3 files changed

+103
-23
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics;
56
using System.IO.Pipelines;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.AspNetCore.Connections;
910

1011
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1112
{
13+
using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException;
14+
1215
internal sealed class Http1ContentLengthMessageBody : Http1MessageBody
1316
{
1417
private ReadResult _readResult;
@@ -18,6 +21,7 @@ internal sealed class Http1ContentLengthMessageBody : Http1MessageBody
1821
private bool _isReading;
1922
private int _userCanceled;
2023
private bool _finalAdvanceCalled;
24+
private bool _cannotResetInputPipe;
2125

2226
public Http1ContentLengthMessageBody(bool keepAlive, long contentLength, Http1Connection context)
2327
: base(context)
@@ -35,15 +39,19 @@ public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationTo
3539

3640
public override async ValueTask<ReadResult> ReadAsyncInternal(CancellationToken cancellationToken = default)
3741
{
38-
if (_isReading)
39-
{
40-
throw new InvalidOperationException("Reading is already in progress.");
41-
}
42+
VerifyIsNotReading();
4243

4344
if (_readCompleted)
4445
{
4546
_isReading = true;
46-
return new ReadResult(_readResult.Buffer, Interlocked.Exchange(ref _userCanceled, 0) == 1, _readResult.IsCompleted);
47+
return new ReadResult(_readResult.Buffer, Interlocked.Exchange(ref _userCanceled, 0) == 1, isCompleted: true);
48+
}
49+
50+
// The issue is that TryRead can get a canceled read result
51+
// which is unknown to StartTimingReadAsync.
52+
if (_context.RequestTimedOut)
53+
{
54+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
4755
}
4856

4957
TryStart();
@@ -54,12 +62,6 @@ public override async ValueTask<ReadResult> ReadAsyncInternal(CancellationToken
5462
// We internally track an int for that.
5563
while (true)
5664
{
57-
// The issue is that TryRead can get a canceled read result
58-
// which is unknown to StartTimingReadAsync.
59-
if (_context.RequestTimedOut)
60-
{
61-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
62-
}
6365

6466
try
6567
{
@@ -76,10 +78,14 @@ public override async ValueTask<ReadResult> ReadAsyncInternal(CancellationToken
7678

7779
void ResetReadingState()
7880
{
79-
_isReading = false;
8081
// Reset the timing read here for the next call to read.
8182
StopTimingRead(0);
82-
_context.Input.AdvanceTo(_readResult.Buffer.Start);
83+
84+
if (!_cannotResetInputPipe)
85+
{
86+
_isReading = false;
87+
_context.Input.AdvanceTo(_readResult.Buffer.Start);
88+
}
8389
}
8490

8591
if (_context.RequestTimedOut)
@@ -95,7 +101,10 @@ void ResetReadingState()
95101
}
96102

97103
// Ignore the canceled readResult if it wasn't canceled by the user.
98-
if (!_readResult.IsCanceled || Interlocked.Exchange(ref _userCanceled, 0) == 1)
104+
// Normally we do not return a canceled ReadResult unless CancelPendingRead was called on the request body PipeReader itself,
105+
// but if the last call to AdvanceTo examined data it did not consume, we cannot reset the state of the Input pipe.
106+
// https://github.com/dotnet/aspnetcore/issues/19476
107+
if (!_readResult.IsCanceled || Interlocked.Exchange(ref _userCanceled, 0) == 1 || _cannotResetInputPipe)
99108
{
100109
var returnedReadResultLength = CreateReadResultFromConnectionReadResult();
101110

@@ -124,18 +133,20 @@ public override bool TryRead(out ReadResult readResult)
124133

125134
public override bool TryReadInternal(out ReadResult readResult)
126135
{
127-
if (_isReading)
128-
{
129-
throw new InvalidOperationException("Reading is already in progress.");
130-
}
136+
VerifyIsNotReading();
131137

132138
if (_readCompleted)
133139
{
134140
_isReading = true;
135-
readResult = new ReadResult(_readResult.Buffer, Interlocked.Exchange(ref _userCanceled, 0) == 1, _readResult.IsCompleted);
141+
readResult = new ReadResult(_readResult.Buffer, Interlocked.Exchange(ref _userCanceled, 0) == 1, isCompleted: true);
136142
return true;
137143
}
138144

145+
if (_context.RequestTimedOut)
146+
{
147+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
148+
}
149+
139150
TryStart();
140151

141152
// The while(true) because we don't want to return a canceled ReadResult if the user themselves didn't cancel it.
@@ -147,7 +158,7 @@ public override bool TryReadInternal(out ReadResult readResult)
147158
return false;
148159
}
149160

150-
if (!_readResult.IsCanceled || Interlocked.Exchange(ref _userCanceled, 0) == 1)
161+
if (!_readResult.IsCanceled || Interlocked.Exchange(ref _userCanceled, 0) == 1 || _cannotResetInputPipe)
151162
{
152163
break;
153164
}
@@ -157,7 +168,15 @@ public override bool TryReadInternal(out ReadResult readResult)
157168

158169
if (_readResult.IsCompleted)
159170
{
160-
_context.Input.AdvanceTo(_readResult.Buffer.Start);
171+
if (_cannotResetInputPipe)
172+
{
173+
_isReading = true;
174+
}
175+
else
176+
{
177+
_context.Input.AdvanceTo(_readResult.Buffer.Start);
178+
}
179+
161180
ThrowUnexpectedEndOfRequestContent();
162181
}
163182

@@ -214,7 +233,7 @@ public override void AdvanceTo(SequencePosition consumed, SequencePosition exami
214233
if (_readCompleted)
215234
{
216235
// If the old stored _readResult was canceled, it's already been observed. Do not store a canceled read result permanently.
217-
_readResult = new ReadResult(_readResult.Buffer.Slice(consumed, _readResult.Buffer.End), isCanceled: false, _readCompleted);
236+
_readResult = new ReadResult(_readResult.Buffer.Slice(consumed, _readResult.Buffer.End), isCanceled: false, isCompleted: true);
218237

219238
if (!_finalAdvanceCalled && _readResult.Buffer.Length == 0)
220239
{
@@ -226,6 +245,10 @@ public override void AdvanceTo(SequencePosition consumed, SequencePosition exami
226245
return;
227246
}
228247

248+
// If consumed != examined, we cannot reset _context.Input back to a non-reading state after the next call to ReadAsync
249+
// simply by calling _context.Input.AdvanceTo(_readResult.Buffer.Start) because the DefaultPipeReader will complain that
250+
// "The examined position cannot be less than the previously examined position."
251+
_cannotResetInputPipe = !consumed.Equals(examined);
229252
_unexaminedInputLength -= TrackConsumedAndExaminedBytes(_readResult, consumed, examined);
230253
_context.Input.AdvanceTo(consumed, examined);
231254
}
@@ -255,5 +278,29 @@ protected override Task OnStopAsync()
255278
Complete(null);
256279
return Task.CompletedTask;
257280
}
281+
282+
[StackTraceHidden]
283+
private void VerifyIsNotReading()
284+
{
285+
if (!_isReading)
286+
{
287+
return;
288+
}
289+
290+
if (_cannotResetInputPipe)
291+
{
292+
if (_readResult.IsCompleted)
293+
{
294+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent);
295+
}
296+
297+
if (_context.RequestTimedOut)
298+
{
299+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
300+
}
301+
}
302+
303+
throw new InvalidOperationException("Reading is already in progress.");
304+
}
258305
}
259306
}

src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ protected async Task OnConsumeAsyncAwaited()
8989
AdvanceTo(result.Buffer.End);
9090
} while (!result.IsCompleted);
9191
}
92-
catch (Microsoft.AspNetCore.Http.BadHttpRequestException ex)
92+
catch (BadHttpRequestException ex)
9393
{
9494
_context.SetBadRequestState(ex);
9595
}

src/Servers/Kestrel/Core/test/MessageBodyTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,39 @@ public async Task UnexpectedEndOfRequestContentIsRepeatedlyThrownForContentLengt
12261226

12271227
input.Application.Output.Complete();
12281228

1229+
#pragma warning disable CS0618 // Type or member is obsolete
1230+
var ex0 = Assert.Throws<BadHttpRequestException>(() => reader.TryRead(out var readResult));
1231+
var ex1 = Assert.Throws<BadHttpRequestException>(() => reader.TryRead(out var readResult));
1232+
var ex2 = await Assert.ThrowsAsync<BadHttpRequestException>(() => reader.ReadAsync().AsTask());
1233+
var ex3 = await Assert.ThrowsAsync<BadHttpRequestException>(() => reader.ReadAsync().AsTask());
1234+
#pragma warning restore CS0618 // Type or member is obsolete
1235+
1236+
Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex0.Reason);
1237+
Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex1.Reason);
1238+
Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex2.Reason);
1239+
Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex3.Reason);
1240+
1241+
await body.StopAsync();
1242+
}
1243+
}
1244+
1245+
[Fact]
1246+
public async Task UnexpectedEndOfRequestContentIsRepeatedlyThrownForContentLengthBodyAfterExaminingButNotConsumingBytes()
1247+
{
1248+
using (var input = new TestInput())
1249+
{
1250+
var body = Http1MessageBody.For(HttpVersion.Http11, new HttpRequestHeaders { HeaderContentLength = "5" }, input.Http1Connection);
1251+
var reader = new HttpRequestPipeReader();
1252+
reader.StartAcceptingReads(body);
1253+
1254+
await input.Application.Output.WriteAsync(new byte[] { 0 });
1255+
1256+
var readResult = await reader.ReadAsync();
1257+
1258+
reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
1259+
1260+
input.Application.Output.Complete();
1261+
12291262
#pragma warning disable CS0618 // Type or member is obsolete
12301263
var ex0 = Assert.Throws<BadHttpRequestException>(() => reader.TryRead(out var readResult));
12311264
var ex1 = Assert.Throws<BadHttpRequestException>(() => reader.TryRead(out var readResult));

0 commit comments

Comments
 (0)