diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs index 87d408feaa70..f6e1279fc4ff 100644 --- a/src/Hosting/TestHost/src/HttpContextBuilder.cs +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -83,7 +83,14 @@ internal void RegisterResponseReadCompleteCallback(Action responseR /// internal Task SendAsync(CancellationToken cancellationToken) { - var registration = cancellationToken.Register(ClientInitiatedAbort); + var registration = cancellationToken.Register(() => + { + ClientInitiatedAbort(); + + // The client gave up on this request. Fail its task now, the way a real server would, instead of + // waiting for an application that may never check RequestAborted. + _responseTcs.TrySetException(new OperationCanceledException("The request was canceled.", cancellationToken)); + }); // Everything inside this function happens in the SERVER's execution context (unless PreserveExecutionContext is true) async Task RunRequestAsync() diff --git a/src/Hosting/TestHost/test/TestClientTests.cs b/src/Hosting/TestHost/test/TestClientTests.cs index 5d3d774a3e37..c3af08d2750f 100644 --- a/src/Hosting/TestHost/test/TestClientTests.cs +++ b/src/Hosting/TestHost/test/TestClientTests.cs @@ -1081,6 +1081,40 @@ public async Task ClientCancellationAbortsRequest() var exception = await Assert.ThrowsAnyAsync(async () => await tcs.Task); } + [Fact] + public async Task ClientCancellationThrowsWithoutWaitingForApplication() + { + var applicationStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var applicationCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var builder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => app.Run(async ctx => + { + // The application never observes RequestAborted and keeps running after the client gives up. + applicationStarted.SetResult(); + await applicationCompleted.Task; + })); + }); + using var host = builder.Build(); + await host.StartAsync(); + + using var server = host.GetTestServer(); + using var client = server.CreateClient(); + using var cts = new CancellationTokenSource(); + + var requestTask = client.GetAsync("http://localhost:12345", cts.Token); + await applicationStarted.Task.DefaultTimeout(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(() => requestTask.DefaultTimeout()); + + applicationCompleted.SetResult(); + } + [Fact] public async Task AsyncLocalValueOnClientIsNotPreserved() {