Skip to content

Commit f17520c

Browse files
authored
Start each request on fresh ExecutionContext (dotnet#14146)
1 parent 746ac6b commit f17520c

File tree

2 files changed

+375
-155
lines changed

2 files changed

+375
-155
lines changed

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

Lines changed: 112 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,8 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
547547
{
548548
try
549549
{
550+
// We run the request processing loop in a seperate async method so per connection
551+
// exception handling doesn't complicate the generated asm for the loop.
550552
await ProcessRequests(application);
551553
}
552554
catch (BadHttpRequestException ex)
@@ -624,103 +626,111 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
624626

625627
InitializeBodyControl(messageBody);
626628

627-
var context = application.CreateContext(this);
629+
// We run user controlled request processing in a seperate async method
630+
// so any changes made to ExecutionContext are undone when it returns and
631+
// each request starts with a fresh ExecutionContext state.
632+
await ProcessRequest(application);
628633

629-
try
630-
{
631-
KestrelEventSource.Log.RequestStart(this);
632-
633-
// Run the application code for this request
634-
await application.ProcessRequestAsync(context);
635-
636-
// Trigger OnStarting if it hasn't been called yet and the app hasn't
637-
// already failed. If an OnStarting callback throws we can go through
638-
// our normal error handling in ProduceEnd.
639-
// https://github.com/aspnet/KestrelHttpServer/issues/43
640-
if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0)
641-
{
642-
await FireOnStarting();
643-
}
644-
645-
if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException))
646-
{
647-
ReportApplicationError(lengthException);
648-
}
649-
}
650-
catch (BadHttpRequestException ex)
634+
// Even for non-keep-alive requests, try to consume the entire body to avoid RSTs.
635+
if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty)
651636
{
652-
// Capture BadHttpRequestException for further processing
653-
// This has to be caught here so StatusCode is set properly before disposing the HttpContext
654-
// (DisposeContext logs StatusCode).
655-
SetBadRequestState(ex);
656-
ReportApplicationError(ex);
637+
await messageBody.ConsumeAsync();
657638
}
658-
catch (Exception ex)
639+
640+
if (HasStartedConsumingRequestBody)
659641
{
660-
ReportApplicationError(ex);
642+
await messageBody.StopAsync();
661643
}
644+
}
645+
}
662646

663-
KestrelEventSource.Log.RequestStop(this);
647+
private async ValueTask ProcessRequest<TContext>(IHttpApplication<TContext> application)
648+
{
649+
var context = application.CreateContext(this);
664650

665-
// At this point all user code that needs use to the request or response streams has completed.
666-
// Using these streams in the OnCompleted callback is not allowed.
667-
try
668-
{
669-
await _bodyControl.StopAsync();
670-
}
671-
catch (Exception ex)
672-
{
673-
// BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing
674-
// enough bytes to satisfy the specified Content-Length. This risks double-logging the exception,
675-
// but this scenario generally indicates an app bug, so I don't want to risk not logging it.
676-
ReportApplicationError(ex);
677-
}
651+
try
652+
{
653+
KestrelEventSource.Log.RequestStart(this);
678654

679-
// 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down.
680-
if (_requestRejectedException == null)
655+
// Run the application code for this request
656+
await application.ProcessRequestAsync(context);
657+
658+
// Trigger OnStarting if it hasn't been called yet and the app hasn't
659+
// already failed. If an OnStarting callback throws we can go through
660+
// our normal error handling in ProduceEnd.
661+
// https://github.com/aspnet/KestrelHttpServer/issues/43
662+
if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0)
681663
{
682-
if (!_connectionAborted)
683-
{
684-
// Call ProduceEnd() before consuming the rest of the request body to prevent
685-
// delaying clients waiting for the chunk terminator:
686-
//
687-
// https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663
688-
//
689-
// This also prevents the 100 Continue response from being sent if the app
690-
// never tried to read the body.
691-
// https://github.com/aspnet/KestrelHttpServer/issues/2102
692-
//
693-
// ProduceEnd() must be called before _application.DisposeContext(), to ensure
694-
// HttpContext.Response.StatusCode is correctly set when
695-
// IHttpContextFactory.Dispose(HttpContext) is called.
696-
await ProduceEnd();
697-
}
698-
else if (!HasResponseStarted)
699-
{
700-
// If the request was aborted and no response was sent, there's no
701-
// meaningful status code to log.
702-
StatusCode = 0;
703-
}
664+
await FireOnStarting();
704665
}
705666

706-
if (_onCompleted?.Count > 0)
667+
if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException))
707668
{
708-
await FireOnCompleted();
669+
ReportApplicationError(lengthException);
709670
}
671+
}
672+
catch (BadHttpRequestException ex)
673+
{
674+
// Capture BadHttpRequestException for further processing
675+
// This has to be caught here so StatusCode is set properly before disposing the HttpContext
676+
// (DisposeContext logs StatusCode).
677+
SetBadRequestState(ex);
678+
ReportApplicationError(ex);
679+
}
680+
catch (Exception ex)
681+
{
682+
ReportApplicationError(ex);
683+
}
710684

711-
application.DisposeContext(context, _applicationException);
685+
KestrelEventSource.Log.RequestStop(this);
712686

713-
// Even for non-keep-alive requests, try to consume the entire body to avoid RSTs.
714-
if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty)
687+
// At this point all user code that needs use to the request or response streams has completed.
688+
// Using these streams in the OnCompleted callback is not allowed.
689+
try
690+
{
691+
await _bodyControl.StopAsync();
692+
}
693+
catch (Exception ex)
694+
{
695+
// BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing
696+
// enough bytes to satisfy the specified Content-Length. This risks double-logging the exception,
697+
// but this scenario generally indicates an app bug, so I don't want to risk not logging it.
698+
ReportApplicationError(ex);
699+
}
700+
701+
// 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down.
702+
if (_requestRejectedException == null)
703+
{
704+
if (!_connectionAborted)
715705
{
716-
await messageBody.ConsumeAsync();
706+
// Call ProduceEnd() before consuming the rest of the request body to prevent
707+
// delaying clients waiting for the chunk terminator:
708+
//
709+
// https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663
710+
//
711+
// This also prevents the 100 Continue response from being sent if the app
712+
// never tried to read the body.
713+
// https://github.com/aspnet/KestrelHttpServer/issues/2102
714+
//
715+
// ProduceEnd() must be called before _application.DisposeContext(), to ensure
716+
// HttpContext.Response.StatusCode is correctly set when
717+
// IHttpContextFactory.Dispose(HttpContext) is called.
718+
await ProduceEnd();
717719
}
718-
719-
if (HasStartedConsumingRequestBody)
720+
else if (!HasResponseStarted)
720721
{
721-
await messageBody.StopAsync();
722+
// If the request was aborted and no response was sent, there's no
723+
// meaningful status code to log.
724+
StatusCode = 0;
722725
}
723726
}
727+
728+
if (_onCompleted?.Count > 0)
729+
{
730+
await FireOnCompleted();
731+
}
732+
733+
application.DisposeContext(context, _applicationException);
724734
}
725735

726736
public void OnStarting(Func<object, Task> callback, object state)
@@ -749,108 +759,55 @@ public void OnCompleted(Func<object, Task> callback, object state)
749759
protected Task FireOnStarting()
750760
{
751761
var onStarting = _onStarting;
752-
753-
if (onStarting == null || onStarting.Count == 0)
754-
{
755-
return Task.CompletedTask;
756-
}
757-
else
762+
if (onStarting?.Count > 0)
758763
{
759-
return FireOnStartingMayAwait(onStarting);
764+
return ProcessEvents(this, onStarting);
760765
}
761-
}
762766

763-
private Task FireOnStartingMayAwait(Stack<KeyValuePair<Func<object, Task>, object>> onStarting)
764-
{
765-
try
767+
return Task.CompletedTask;
768+
769+
static async Task ProcessEvents(HttpProtocol protocol, Stack<KeyValuePair<Func<object, Task>, object>> events)
766770
{
767-
while (onStarting.TryPop(out var entry))
771+
// Try/Catch is outside the loop as any error that occurs is before the request starts.
772+
// So we want to report it as an ApplicationError to fail the request and not process more events.
773+
try
768774
{
769-
var task = entry.Key.Invoke(entry.Value);
770-
if (!task.IsCompletedSuccessfully)
775+
while (events.TryPop(out var entry))
771776
{
772-
return FireOnStartingAwaited(task, onStarting);
777+
await entry.Key.Invoke(entry.Value);
773778
}
774779
}
775-
}
776-
catch (Exception ex)
777-
{
778-
ReportApplicationError(ex);
779-
}
780-
781-
return Task.CompletedTask;
782-
}
783-
784-
private async Task FireOnStartingAwaited(Task currentTask, Stack<KeyValuePair<Func<object, Task>, object>> onStarting)
785-
{
786-
try
787-
{
788-
await currentTask;
789-
790-
while (onStarting.TryPop(out var entry))
780+
catch (Exception ex)
791781
{
792-
await entry.Key.Invoke(entry.Value);
782+
protocol.ReportApplicationError(ex);
793783
}
794784
}
795-
catch (Exception ex)
796-
{
797-
ReportApplicationError(ex);
798-
}
799785
}
800786

801787
protected Task FireOnCompleted()
802788
{
803789
var onCompleted = _onCompleted;
804-
805-
if (onCompleted == null || onCompleted.Count == 0)
790+
if (onCompleted?.Count > 0)
806791
{
807-
return Task.CompletedTask;
792+
return ProcessEvents(this, onCompleted);
808793
}
809794

810-
return FireOnCompletedMayAwait(onCompleted);
811-
}
795+
return Task.CompletedTask;
812796

813-
private Task FireOnCompletedMayAwait(Stack<KeyValuePair<Func<object, Task>, object>> onCompleted)
814-
{
815-
while (onCompleted.TryPop(out var entry))
797+
static async Task ProcessEvents(HttpProtocol protocol, Stack<KeyValuePair<Func<object, Task>, object>> events)
816798
{
817-
try
799+
// Try/Catch is inside the loop as any error that occurs is after the request has finished.
800+
// So we will just log it and keep processing the events, as the completion has already happened.
801+
while (events.TryPop(out var entry))
818802
{
819-
var task = entry.Key.Invoke(entry.Value);
820-
if (!task.IsCompletedSuccessfully)
803+
try
821804
{
822-
return FireOnCompletedAwaited(task, onCompleted);
805+
await entry.Key.Invoke(entry.Value);
806+
}
807+
catch (Exception ex)
808+
{
809+
protocol.Log.ApplicationError(protocol.ConnectionId, protocol.TraceIdentifier, ex);
823810
}
824-
}
825-
catch (Exception ex)
826-
{
827-
ReportApplicationError(ex);
828-
}
829-
}
830-
831-
return Task.CompletedTask;
832-
}
833-
834-
private async Task FireOnCompletedAwaited(Task currentTask, Stack<KeyValuePair<Func<object, Task>, object>> onCompleted)
835-
{
836-
try
837-
{
838-
await currentTask;
839-
}
840-
catch (Exception ex)
841-
{
842-
Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
843-
}
844-
845-
while (onCompleted.TryPop(out var entry))
846-
{
847-
try
848-
{
849-
await entry.Key.Invoke(entry.Value);
850-
}
851-
catch (Exception ex)
852-
{
853-
Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
854811
}
855812
}
856813
}

0 commit comments

Comments
 (0)