Reset endpoint and route values during exception handling.

- We initially did this change as part of EndpointRouting but the impact of that change resulted in a variety of performance regressions. To mitigate the impact of resetting state for a request we now only reset the state when an exception has occurred in a way that does not require any additional state machines to be allocated.
- Added a test to validate that http context state gets reset on exception handling.

#12897
This commit is contained in:
N. Taylor Mullen 2019-08-12 13:41:36 -07:00
parent 4f6022323b
commit 1f0641f5c0
2 changed files with 101 additions and 1 deletions

View File

@ -7,6 +7,7 @@ using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
@ -103,7 +104,8 @@ namespace Microsoft.AspNetCore.Diagnostics
}
try
{
context.Response.Clear();
ClearHttpContext(context);
var exceptionHandlerFeature = new ExceptionHandlerFeature()
{
Error = edi.SourceException,
@ -137,6 +139,17 @@ namespace Microsoft.AspNetCore.Diagnostics
edi.Throw(); // Re-throw the original if we couldn't handle it
}
private static void ClearHttpContext(HttpContext context)
{
context.Response.Clear();
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
// the endpoint and route values to ensure things are re-calculated.
context.SetEndpoint(endpoint: null);
var routeValuesFeature = context.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
}
private static Task ClearCacheHeaders(object state)
{
var headers = ((HttpResponse)state).Headers;

View File

@ -0,0 +1,87 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Diagnostics
{
public class ExceptionHandlerMiddlewareTest
{
[Fact]
public async Task Invoke_ExceptionThrownResultsInClearedRouteValuesAndEndpoint()
{
// Arrange
var httpContext = CreateHttpContext();
httpContext.SetEndpoint(new Endpoint((_) => Task.CompletedTask, new EndpointMetadataCollection(), "Test"));
httpContext.Request.RouteValues["John"] = "Doe";
var optionsAccessor = CreateOptionsAccessor(
exceptionHandler: context =>
{
Assert.Empty(context.Request.RouteValues);
Assert.Null(context.GetEndpoint());
return Task.CompletedTask;
});
var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor);
// Act & Assert
await middleware.Invoke(httpContext);
}
private HttpContext CreateHttpContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = new TestServiceProvider()
};
return httpContext;
}
private IOptions<ExceptionHandlerOptions> CreateOptionsAccessor(
RequestDelegate exceptionHandler = null,
string exceptionHandlingPath = null)
{
exceptionHandler ??= c => Task.CompletedTask;
var options = new ExceptionHandlerOptions()
{
ExceptionHandler = exceptionHandler,
ExceptionHandlingPath = exceptionHandlingPath,
};
var optionsAccessor = Mock.Of<IOptions<ExceptionHandlerOptions>>(o => o.Value == options);
return optionsAccessor;
}
private ExceptionHandlerMiddleware CreateMiddleware(
RequestDelegate next,
IOptions<ExceptionHandlerOptions> options)
{
next ??= c => Task.CompletedTask;
var listener = new DiagnosticListener("Microsoft.AspNetCore");
var middleware = new ExceptionHandlerMiddleware(
next,
NullLoggerFactory.Instance,
options,
listener);
return middleware;
}
private class TestServiceProvider : IServiceProvider
{
public object GetService(Type serviceType)
{
throw new NotImplementedException();
}
}
}
}