diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 6b392965696a2bcac15d50045e4ec9b884f54306..ebbfdc962b8ef8253b8a6d2d15a8ce2bafee10a3 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -254,7 +254,6 @@ internal partial class RequestContext : NativeRequestContext, IThreadPoolWorkIte { Response.StatusCode = status; Response.ContentLength = 0; - Dispose(); } internal unsafe void Delegate(DelegationRule destination) diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 016190fd3a47822563047c56b55ec7e3b6119016..6d7eecab820495a0187815d802df090c426a99e2 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -32,10 +32,12 @@ internal sealed partial class RequestContext<TContext> : RequestContext where TC if (messagePump.Stopping) { SetFatalResponse(503); + Dispose(); return; } TContext? context = default; + Exception? applicationException = null; messagePump.IncrementOutstandingRequest(); try { @@ -49,16 +51,12 @@ internal sealed partial class RequestContext<TContext> : RequestContext where TC { await OnCompleted(); } - application.DisposeContext(context, null); - Dispose(); } catch (Exception ex) { + applicationException = ex; + Log.RequestProcessError(Logger, ex); - if (context != null) - { - application.DisposeContext(context, ex); - } if (Response.HasStarted) { // Otherwise the default is Cancel = 0x8 (h2) or 0x010c (h3). @@ -94,11 +92,18 @@ internal sealed partial class RequestContext<TContext> : RequestContext where TC } finally { + if (context != null) + { + application.DisposeContext(context, applicationException); + } + if (messagePump.DecrementOutstandingRequest() == 0 && messagePump.Stopping) { Log.RequestsDrained(Logger); messagePump.SetShutdownSignal(); } + + Dispose(); } } catch (Exception ex) diff --git a/src/Servers/test/FunctionalTests/HelloWorldTest.cs b/src/Servers/test/FunctionalTests/HelloWorldTest.cs index 23cdf5eee28fdc14aa61ce4804e3122195342c62..04a0384941f091edf33a1b62be014cca76cb6593 100644 --- a/src/Servers/test/FunctionalTests/HelloWorldTest.cs +++ b/src/Servers/test/FunctionalTests/HelloWorldTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.AspNetCore.Testing; @@ -108,4 +109,56 @@ public class HelloWorldTests : LoggedTest } } } + + public static TestMatrix SelfHostTestVariants + => TestMatrix.ForServers(ServerType.Kestrel, ServerType.HttpSys) + .WithTfms(Tfm.Default) + .WithApplicationTypes(ApplicationType.Portable) + .WithAllHostingModels() + .WithAllArchitectures(); + + [ConditionalTheory] + [MemberData(nameof(SelfHostTestVariants))] + public async Task ApplicationException(TestVariant variant) + { + var testName = $"ApplicationException_{variant.Server}_{variant.Tfm}_{variant.Architecture}_{variant.ApplicationType}"; + using (StartLog(out var loggerFactory, LogLevel.Debug, testName)) + { + var logger = loggerFactory.CreateLogger("ApplicationException"); + + var deploymentParameters = new DeploymentParameters(variant) + { + ApplicationPath = Helpers.GetApplicationPath() + }; + + var output = string.Empty; + using (var deployer = new SelfHostDeployer(deploymentParameters, loggerFactory)) + { + deployer.ProcessOutputListener = (data) => + { + if (!string.IsNullOrWhiteSpace(data)) + { + output += data + '\n'; + } + }; + + var deploymentResult = await deployer.DeployAsync(); + + // Request to base address and check if various parts of the body are rendered & measure the cold startup time. + using (var response = await RetryHelper.RetryRequest(() => + { + return deploymentResult.HttpClient.GetAsync("/throwexception"); + }, logger, deploymentResult.HostShutdownToken)) + { + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Empty(body); + } + } + // Output should contain the ApplicationException and the 500 status code + Assert.Contains("System.ApplicationException: Application exception", output); + Assert.Contains("/throwexception - - - 500", output); + } + } } diff --git a/src/Servers/testassets/ServerComparison.TestSites/Startup.cs b/src/Servers/testassets/ServerComparison.TestSites/Startup.cs index 61e3e3b27ee61ccc55c9b12ae3a7714deddd7355..93ef6b8df89f58234e227e7357ca4dcdf5acc8b2 100644 --- a/src/Servers/testassets/ServerComparison.TestSites/Startup.cs +++ b/src/Servers/testassets/ServerComparison.TestSites/Startup.cs @@ -12,6 +12,14 @@ public class Startup { public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { + app.Map("/throwexception", subApp => + { + subApp.Run(context => + { + throw new ApplicationException("Application exception"); + }); + }); + app.Run(ctx => { return ctx.Response.WriteAsync("Hello World " + RuntimeInformation.ProcessArchitecture);