2
2
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3
3
4
4
using System ;
5
+ using System . Diagnostics ;
5
6
using System . IO . Pipelines ;
6
7
using System . Threading ;
7
8
using System . Threading . Tasks ;
8
9
using Microsoft . AspNetCore . Connections ;
9
10
10
11
namespace Microsoft . AspNetCore . Server . Kestrel . Core . Internal . Http
11
12
{
13
+ using BadHttpRequestException = Microsoft . AspNetCore . Http . BadHttpRequestException ;
14
+
12
15
internal sealed class Http1ContentLengthMessageBody : Http1MessageBody
13
16
{
14
17
private ReadResult _readResult ;
@@ -18,6 +21,7 @@ internal sealed class Http1ContentLengthMessageBody : Http1MessageBody
18
21
private bool _isReading ;
19
22
private int _userCanceled ;
20
23
private bool _finalAdvanceCalled ;
24
+ private bool _cannotResetInputPipe ;
21
25
22
26
public Http1ContentLengthMessageBody ( bool keepAlive , long contentLength , Http1Connection context )
23
27
: base ( context )
@@ -35,15 +39,19 @@ public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationTo
35
39
36
40
public override async ValueTask < ReadResult > ReadAsyncInternal ( CancellationToken cancellationToken = default )
37
41
{
38
- if ( _isReading )
39
- {
40
- throw new InvalidOperationException ( "Reading is already in progress." ) ;
41
- }
42
+ VerifyIsNotReading ( ) ;
42
43
43
44
if ( _readCompleted )
44
45
{
45
46
_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 ) ;
47
55
}
48
56
49
57
TryStart ( ) ;
@@ -54,12 +62,6 @@ public override async ValueTask<ReadResult> ReadAsyncInternal(CancellationToken
54
62
// We internally track an int for that.
55
63
while ( true )
56
64
{
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
- }
63
65
64
66
try
65
67
{
@@ -76,10 +78,14 @@ public override async ValueTask<ReadResult> ReadAsyncInternal(CancellationToken
76
78
77
79
void ResetReadingState ( )
78
80
{
79
- _isReading = false ;
80
81
// Reset the timing read here for the next call to read.
81
82
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
+ }
83
89
}
84
90
85
91
if ( _context . RequestTimedOut )
@@ -95,7 +101,10 @@ void ResetReadingState()
95
101
}
96
102
97
103
// 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 )
99
108
{
100
109
var returnedReadResultLength = CreateReadResultFromConnectionReadResult ( ) ;
101
110
@@ -124,18 +133,20 @@ public override bool TryRead(out ReadResult readResult)
124
133
125
134
public override bool TryReadInternal ( out ReadResult readResult )
126
135
{
127
- if ( _isReading )
128
- {
129
- throw new InvalidOperationException ( "Reading is already in progress." ) ;
130
- }
136
+ VerifyIsNotReading ( ) ;
131
137
132
138
if ( _readCompleted )
133
139
{
134
140
_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 ) ;
136
142
return true ;
137
143
}
138
144
145
+ if ( _context . RequestTimedOut )
146
+ {
147
+ KestrelBadHttpRequestException . Throw ( RequestRejectionReason . RequestBodyTimeout ) ;
148
+ }
149
+
139
150
TryStart ( ) ;
140
151
141
152
// 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)
147
158
return false ;
148
159
}
149
160
150
- if ( ! _readResult . IsCanceled || Interlocked . Exchange ( ref _userCanceled , 0 ) == 1 )
161
+ if ( ! _readResult . IsCanceled || Interlocked . Exchange ( ref _userCanceled , 0 ) == 1 || _cannotResetInputPipe )
151
162
{
152
163
break ;
153
164
}
@@ -157,7 +168,15 @@ public override bool TryReadInternal(out ReadResult readResult)
157
168
158
169
if ( _readResult . IsCompleted )
159
170
{
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
+
161
180
ThrowUnexpectedEndOfRequestContent ( ) ;
162
181
}
163
182
@@ -214,7 +233,7 @@ public override void AdvanceTo(SequencePosition consumed, SequencePosition exami
214
233
if ( _readCompleted )
215
234
{
216
235
// 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 ) ;
218
237
219
238
if ( ! _finalAdvanceCalled && _readResult . Buffer . Length == 0 )
220
239
{
@@ -226,6 +245,10 @@ public override void AdvanceTo(SequencePosition consumed, SequencePosition exami
226
245
return ;
227
246
}
228
247
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 ) ;
229
252
_unexaminedInputLength -= TrackConsumedAndExaminedBytes ( _readResult , consumed , examined ) ;
230
253
_context . Input . AdvanceTo ( consumed , examined ) ;
231
254
}
@@ -255,5 +278,29 @@ protected override Task OnStopAsync()
255
278
Complete ( null ) ;
256
279
return Task . CompletedTask ;
257
280
}
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
+ }
258
305
}
259
306
}
0 commit comments