Skip to content

Commit bda52f7

Browse files
author
John Luo
authored
Merge pull request #26994 from dotnet-maestro-bot/merge/release/3.1-to-master
[automated] Merge branch 'release/3.1' => 'master'
2 parents 63199b4 + 98a94be commit bda52f7

File tree

3 files changed

+103
-3
lines changed

3 files changed

+103
-3
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public static MessageBody For(
192192
}
193193

194194
[StackTraceHidden]
195-
private void ThrowIfReaderCompleted()
195+
protected void ThrowIfReaderCompleted()
196196
{
197197
if (_readerCompleted)
198198
{

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

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1414
/// </summary>
1515
internal sealed class Http1UpgradeMessageBody : Http1MessageBody
1616
{
17+
private int _userCanceled;
18+
1719
public Http1UpgradeMessageBody(Http1Connection context, bool keepAlive)
1820
: base(context, keepAlive)
1921
{
@@ -23,13 +25,26 @@ public Http1UpgradeMessageBody(Http1Connection context, bool keepAlive)
2325
// This returns IsEmpty so we can avoid draining the body (since it's basically an endless stream)
2426
public override bool IsEmpty => true;
2527

28+
public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
29+
{
30+
ThrowIfReaderCompleted();
31+
return ReadAsyncInternal(cancellationToken);
32+
}
33+
34+
public override bool TryRead(out ReadResult result)
35+
{
36+
ThrowIfReaderCompleted();
37+
return TryReadInternal(out result);
38+
}
39+
2640
public override void AdvanceTo(SequencePosition consumed, SequencePosition examined)
2741
{
2842
_context.Input.AdvanceTo(consumed, examined);
2943
}
3044

3145
public override void CancelPendingRead()
3246
{
47+
Interlocked.Exchange(ref _userCanceled, 1);
3348
_context.Input.CancelPendingRead();
3449
}
3550

@@ -45,12 +60,49 @@ public override ValueTask StopAsync()
4560

4661
public override bool TryReadInternal(out ReadResult readResult)
4762
{
48-
return _context.Input.TryRead(out readResult);
63+
// Ignore the canceled readResult unless it was canceled by the user.
64+
do
65+
{
66+
if (!_context.Input.TryRead(out readResult))
67+
{
68+
return false;
69+
}
70+
} while (readResult.IsCanceled && Interlocked.Exchange(ref _userCanceled, 0) == 0);
71+
72+
return true;
4973
}
5074

5175
public override ValueTask<ReadResult> ReadAsyncInternal(CancellationToken cancellationToken = default)
5276
{
53-
return _context.Input.ReadAsync(cancellationToken);
77+
ReadResult readResult;
78+
79+
// Ignore the canceled readResult unless it was canceled by the user.
80+
do
81+
{
82+
var readTask = _context.Input.ReadAsync(cancellationToken);
83+
84+
if (!readTask.IsCompletedSuccessfully)
85+
{
86+
return ReadAsyncInternalAwaited(readTask, cancellationToken);
87+
}
88+
89+
readResult = readTask.GetAwaiter().GetResult();
90+
} while (readResult.IsCanceled && Interlocked.Exchange(ref _userCanceled, 0) == 0);
91+
92+
return new ValueTask<ReadResult>(readResult);
93+
}
94+
95+
private async ValueTask<ReadResult> ReadAsyncInternalAwaited(ValueTask<ReadResult> readTask, CancellationToken cancellationToken = default)
96+
{
97+
var readResult = await readTask;
98+
99+
// Ignore the canceled readResult unless it was canceled by the user.
100+
while (readResult.IsCanceled && Interlocked.Exchange(ref _userCanceled, 0) == 0)
101+
{
102+
readResult = await _context.Input.ReadAsync(cancellationToken);
103+
}
104+
105+
return readResult;
54106
}
55107
}
56108
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/UpgradeTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
using System;
55
using System.IO;
6+
using System.IO.Pipelines;
67
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Connections;
9+
using Microsoft.AspNetCore.Connections.Features;
710
using Microsoft.AspNetCore.Http.Features;
811
using Microsoft.AspNetCore.Server.Kestrel.Core;
912
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -343,6 +346,51 @@ await connection.Receive("HTTP/1.1 101 Switching Protocols",
343346
}
344347
}
345348

349+
[Fact]
350+
public async Task DoesNotThrowGivenCanceledReadResult()
351+
{
352+
var appCompletedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
353+
354+
await using var server = new TestServer(async context =>
355+
{
356+
try
357+
{
358+
var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();
359+
var duplexStream = await upgradeFeature.UpgradeAsync();
360+
361+
// Kestrel will call Transport.Input.CancelPendingRead() during shutdown so idle connections
362+
// can wake up and shutdown gracefully. We manually call CancelPendingRead() to simulate this and
363+
// ensure the Stream returned by UpgradeAsync doesn't throw in this case.
364+
// https://github.com/dotnet/aspnetcore/issues/26482
365+
var connectionTransportFeature = context.Features.Get<IConnectionTransportFeature>();
366+
connectionTransportFeature.Transport.Input.CancelPendingRead();
367+
368+
// Use ReadAsync() instead of CopyToAsync() for this test since IsCanceled is only checked in
369+
// HttpRequestStream.ReadAsync() and not HttpRequestStream.CopyToAsync()
370+
Assert.Equal(0, await duplexStream.ReadAsync(new byte[1]));
371+
appCompletedTcs.SetResult(null);
372+
}
373+
catch (Exception ex)
374+
{
375+
appCompletedTcs.SetException(ex);
376+
throw;
377+
}
378+
},
379+
new TestServiceContext(LoggerFactory));
380+
381+
using (var connection = server.CreateConnection())
382+
{
383+
await connection.SendEmptyGetWithUpgrade();
384+
await connection.Receive("HTTP/1.1 101 Switching Protocols",
385+
"Connection: Upgrade",
386+
$"Date: {server.Context.DateHeaderValue}",
387+
"",
388+
"");
389+
}
390+
391+
await appCompletedTcs.Task.DefaultTimeout();
392+
}
393+
346394
[Fact]
347395
public async Task DoesNotCloseConnectionWithout101Response()
348396
{

0 commit comments

Comments
 (0)