Skip to content

Commit 83bf237

Browse files
justinwyerhalter73
authored andcommitted
#2035 Do not await OnCompleted handlers before sending the Response (#2324)
1 parent 06945ba commit 83bf237

File tree

2 files changed

+109
-15
lines changed

2 files changed

+109
-15
lines changed

src/Kestrel.Core/Internal/Http/HttpProtocol.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -554,11 +554,6 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
554554

555555
PauseStreams();
556556

557-
if (_onCompleted != null)
558-
{
559-
await FireOnCompleted();
560-
}
561-
562557
if (badRequestException == null)
563558
{
564559
// If _requestAbort is set, the connection has already been closed.
@@ -601,6 +596,11 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
601596
}
602597
}
603598

599+
if (_onCompleted != null)
600+
{
601+
await FireOnCompleted();
602+
}
603+
604604
if (badRequestException != null)
605605
{
606606
// Handle BadHttpRequestException thrown during app execution or remaining message body consumption.
@@ -739,10 +739,8 @@ protected Task FireOnCompleted()
739739
{
740740
return Task.CompletedTask;
741741
}
742-
else
743-
{
744-
return FireOnCompletedAwaited(onCompleted);
745-
}
742+
743+
return FireOnCompletedAwaited(onCompleted);
746744
}
747745

748746
private async Task FireOnCompletedAwaited(Stack<KeyValuePair<Func<object, Task>, object>> onCompleted)
@@ -755,7 +753,7 @@ private async Task FireOnCompletedAwaited(Stack<KeyValuePair<Func<object, Task>,
755753
}
756754
catch (Exception ex)
757755
{
758-
ReportApplicationError(ex);
756+
Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
759757
}
760758
}
761759
}

test/Kestrel.FunctionalTests/ResponseTests.cs

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public async Task IgnoreNullHeaderValues(string headerName, StringValues headerV
146146
public async Task OnCompleteCalledEvenWhenOnStartingNotCalled()
147147
{
148148
var onStartingCalled = false;
149-
var onCompletedCalled = false;
149+
var onCompletedTcs = new TaskCompletionSource<object>();
150150

151151
var hostBuilder = TransportSelector.GetWebHostBuilder()
152152
.UseKestrel()
@@ -156,7 +156,10 @@ public async Task OnCompleteCalledEvenWhenOnStartingNotCalled()
156156
app.Run(context =>
157157
{
158158
context.Response.OnStarting(() => Task.Run(() => onStartingCalled = true));
159-
context.Response.OnCompleted(() => Task.Run(() => onCompletedCalled = true));
159+
context.Response.OnCompleted(() => Task.Run(() =>
160+
{
161+
onCompletedTcs.SetResult(null);
162+
}));
160163

161164
// Prevent OnStarting call (see HttpProtocol.ProcessRequestsAsync()).
162165
throw new Exception();
@@ -173,7 +176,7 @@ public async Task OnCompleteCalledEvenWhenOnStartingNotCalled()
173176

174177
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
175178
Assert.False(onStartingCalled);
176-
Assert.True(onCompletedCalled);
179+
await onCompletedTcs.Task.TimeoutAfter(TestConstants.DefaultTimeout);
177180
}
178181
}
179182
}
@@ -294,6 +297,99 @@ public Task ResponseStatusCodeSetBeforeHttpContextDisposedRequestMalformedReadIg
294297
sendMalformedRequest: true);
295298
}
296299

300+
[Fact]
301+
public async Task OnCompletedExceptionShouldNotPreventAResponse()
302+
{
303+
var hostBuilder = TransportSelector.GetWebHostBuilder()
304+
.UseKestrel()
305+
.UseUrls("http://127.0.0.1:0/")
306+
.Configure(app =>
307+
{
308+
app.Run(async context =>
309+
{
310+
context.Response.OnCompleted(_ => throw new Exception(), null);
311+
await context.Response.WriteAsync("hello, world");
312+
});
313+
});
314+
315+
using (var host = hostBuilder.Build())
316+
{
317+
host.Start();
318+
319+
using (var client = new HttpClient())
320+
{
321+
var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/");
322+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
323+
}
324+
}
325+
}
326+
327+
[Fact]
328+
public async Task OnCompletedShouldNotBlockAResponse()
329+
{
330+
var delay = Task.Delay(TestConstants.DefaultTimeout);
331+
var hostBuilder = TransportSelector.GetWebHostBuilder()
332+
.UseKestrel()
333+
.UseUrls("http://127.0.0.1:0/")
334+
.Configure(app =>
335+
{
336+
app.Run(async context =>
337+
{
338+
context.Response.OnCompleted(async () =>
339+
{
340+
await delay;
341+
});
342+
await context.Response.WriteAsync("hello, world");
343+
});
344+
});
345+
346+
using (var host = hostBuilder.Build())
347+
{
348+
host.Start();
349+
350+
using (var client = new HttpClient())
351+
{
352+
var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/");
353+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
354+
Assert.False(delay.IsCompleted);
355+
}
356+
}
357+
}
358+
359+
[Fact]
360+
public async Task InvalidChunkedEncodingInRequestShouldNotBlockOnCompleted()
361+
{
362+
var onCompletedTcs = new TaskCompletionSource<object>();
363+
364+
using (var server = new TestServer(httpContext =>
365+
{
366+
httpContext.Response.OnCompleted(() => Task.Run(() =>
367+
{
368+
onCompletedTcs.SetResult(null);
369+
}));
370+
return Task.CompletedTask;
371+
}))
372+
{
373+
using (var connection = server.CreateConnection())
374+
{
375+
await connection.Send(
376+
"GET / HTTP/1.1",
377+
"Host:",
378+
"Transfer-Encoding: chunked",
379+
"",
380+
"gg");
381+
await connection.ReceiveForcedEnd(
382+
"HTTP/1.1 200 OK",
383+
$"Date: {server.Context.DateHeaderValue}",
384+
"Content-Length: 0",
385+
"",
386+
"");
387+
}
388+
}
389+
390+
await onCompletedTcs.Task.TimeoutAfter(TestConstants.DefaultTimeout);
391+
}
392+
297393
private static async Task ResponseStatusCodeSetBeforeHttpContextDispose(
298394
RequestDelegate handler,
299395
HttpStatusCode? expectedClientStatusCode,
@@ -1988,7 +2084,7 @@ await connection.ReceiveEnd(
19882084

19892085
[Theory]
19902086
[MemberData(nameof(ConnectionAdapterData))]
1991-
public async Task ThrowingInOnCompletedIsLoggedAndClosesConnection(ListenOptions listenOptions)
2087+
public async Task ThrowingInOnCompletedIsLogged(ListenOptions listenOptions)
19922088
{
19932089
var testContext = new TestServiceContext();
19942090

@@ -2024,7 +2120,7 @@ await connection.Send(
20242120
"Host:",
20252121
"",
20262122
"");
2027-
await connection.ReceiveForcedEnd(
2123+
await connection.ReceiveEnd(
20282124
"HTTP/1.1 200 OK",
20292125
$"Date: {testContext.DateHeaderValue}",
20302126
"Content-Length: 11",

0 commit comments

Comments
 (0)