/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs
719 строк
27 KB
Arvin Kahbazi
Set IExceptionHandlerFeature on DeveloperExceptionPageMiddleware (#52688)
10 дек 2025, 10:05
Не верифицирован
10 дек 2025, 10:05
94d8301
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.Metrics; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Moq; namespace Microsoft.AspNetCore.Diagnostics; public class ExceptionHandlerMiddlewareTest : LoggedTest { [Fact] public async Task ExceptionIsSetOnProblemDetailsContext() { // Arrange ExceptionHandlerSuppressDiagnosticsContext suppressContext = null; using var host = new HostBuilder() .ConfigureServices(services => { services.AddProblemDetails(configure => { configure.CustomizeProblemDetails = (context) => { if (context.Exception is not null) { context.ProblemDetails.Extensions.Add("OriginalExceptionMessage", context.Exception.Message); } }; }); }) .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler(new ExceptionHandlerOptions { SuppressDiagnosticsCallback = context => { suppressContext = context; return true; } }); app.Run(context => { throw new Exception("Test exception"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); var request = new HttpRequestMessage(HttpMethod.Get, "/path"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // Act var response = await server.CreateClient().SendAsync(request); // Assert var body = await response.Content.ReadFromJsonAsync<ProblemDetails>(); var originalExceptionMessage = ((JsonElement)body.Extensions["OriginalExceptionMessage"]).GetString(); Assert.Equal("Test exception", originalExceptionMessage); Assert.IsType<Exception>(suppressContext.Exception); Assert.Equal(ExceptionHandledType.ProblemDetailsService, suppressContext.ExceptionHandledBy); } [Fact] public async Task Invoke_ExceptionThrownResultsInClearedRouteValuesAndEndpoint() { // Arrange var sink = new TestSink(); 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, loggerFactory: new TestLoggerFactory(sink, true)); // Act & Assert await middleware.Invoke(httpContext); Assert.Collection(sink.Writes, w => Assert.Equal("UnhandledException", w.EventId.Name)); } [Theory] [InlineData(ExceptionHandledType.ExceptionHandlerDelegate, false)] [InlineData(ExceptionHandledType.ProblemDetailsService, true)] public async Task Invoke_HasExceptionHandler_SuppressDiagnostics_CallbackRun(ExceptionHandledType suppressResult, bool logged) { // Arrange var sink = new TestSink(); var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor( exceptionHandler: context => { context.Features.Set<IHttpResponseFeature>(new TestHttpResponseFeature()); return Task.CompletedTask; }, suppressDiagnosticsCallback: c => c.ExceptionHandledBy == suppressResult); var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, loggerFactory: new TestLoggerFactory(sink, true)); // Act & Assert await middleware.Invoke(httpContext); if (logged) { Assert.Collection(sink.Writes, w => Assert.Equal("UnhandledException", w.EventId.Name)); } else { Assert.Empty(sink.Writes); } } private sealed class TestHttpResponseFeature : HttpResponseFeature { public override bool HasStarted => true; } [Fact] public async Task Invoke_ExceptionHandlerCaptureRouteValuesAndEndpoint() { // Arrange var httpContext = CreateHttpContext(); var endpoint = new Endpoint((_) => Task.CompletedTask, new EndpointMetadataCollection(), "Test"); httpContext.SetEndpoint(endpoint); httpContext.Request.RouteValues["John"] = "Doe"; var optionsAccessor = CreateOptionsAccessor( exceptionHandler: context => { var feature = context.Features.Get<IExceptionHandlerPathFeature>(); Assert.Equal(endpoint, feature.Endpoint); Assert.Equal("Doe", feature.RouteValues["John"]); return Task.CompletedTask; }); var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor); // Act & Assert await middleware.Invoke(httpContext); } [Fact] public async Task IExceptionHandlers_CallNextIfNotHandled() { // Arrange var sink = new TestSink(); var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor(); var exceptionHandlers = new List<IExceptionHandler> { new TestExceptionHandler(false, "1"), new TestExceptionHandler(false, "2"), new TestExceptionHandler(true, "3"), }; var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, loggerFactory: new TestLoggerFactory(sink, true)); // Act & Assert await middleware.Invoke(httpContext); Assert.True(httpContext.Items.ContainsKey("1")); Assert.True(httpContext.Items.ContainsKey("2")); Assert.True(httpContext.Items.ContainsKey("3")); // IExceptionHandlers handling an exception suppress diagnostics by default. Assert.Empty(sink.Writes); } [Theory] [InlineData(null)] [InlineData(true)] [InlineData(false)] public async Task IExceptionHandlers_SuppressDiagnostics_TestLogs(bool? suppressDiagnostics) { // Arrange var sink = new TestSink(); var httpContext = CreateHttpContext(); var metricsTagsFeature = new TestHttpMetricsTagsFeature(); httpContext.Features.Set<IHttpMetricsTagsFeature>(metricsTagsFeature); Func<ExceptionHandlerSuppressDiagnosticsContext, bool> suppressDiagnosticsCallback = null; if (suppressDiagnostics != null) { suppressDiagnosticsCallback = c => suppressDiagnostics.Value; } var optionsAccessor = CreateOptionsAccessor(suppressDiagnosticsCallback: suppressDiagnosticsCallback); var exceptionHandlers = new List<IExceptionHandler> { new TestExceptionHandler(true, "1") }; var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, loggerFactory: new TestLoggerFactory(sink, true)); // Act & Assert await middleware.Invoke(httpContext); Assert.True(httpContext.Items.ContainsKey("1")); if (suppressDiagnostics == null || suppressDiagnostics == true) { Assert.Empty(sink.Writes); Assert.Empty(metricsTagsFeature.Tags); } else { Assert.Collection(sink.Writes, w => Assert.Equal("UnhandledException", w.EventId.Name)); var errorTag = Assert.Single(metricsTagsFeature.Tags); Assert.Equal("error.type", errorTag.Key); Assert.Equal("System.InvalidOperationException", errorTag.Value); } } [Fact] public async Task IExceptionHandlers_SkipIfOneHandle() { // Arrange var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor(); var exceptionHandlers = new List<IExceptionHandler> { new TestExceptionHandler(false, "1"), new TestExceptionHandler(true, "2"), new TestExceptionHandler(true, "3"), }; var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers); // Act & Assert await middleware.Invoke(httpContext); Assert.True(httpContext.Items.ContainsKey("1")); Assert.True(httpContext.Items.ContainsKey("2")); Assert.False(httpContext.Items.ContainsKey("3")); } [Fact] public async Task IExceptionHandlers_CallOptionExceptionHandlerIfNobodyHandles() { // Arrange var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor( (context) => { context.Items["ExceptionHandler"] = true; return Task.CompletedTask; }); var exceptionHandlers = new List<IExceptionHandler> { new TestExceptionHandler(false, "1"), new TestExceptionHandler(false, "2"), new TestExceptionHandler(false, "3"), }; var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers); // Act & Assert await middleware.Invoke(httpContext); Assert.True(httpContext.Items.ContainsKey("1")); Assert.True(httpContext.Items.ContainsKey("2")); Assert.True(httpContext.Items.ContainsKey("3")); Assert.True(httpContext.Items.ContainsKey("ExceptionHandler")); } [Fact] public async Task Metrics_NoExceptionThrown() { // Arrange var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor(); var meterFactory = new TestMeterFactory(); var exceptionHandlers = new List<IExceptionHandler> { new TestExceptionHandler(true, "1") }; var middleware = CreateMiddleware(_ => Task.CompletedTask, optionsAccessor, exceptionHandlers, meterFactory); var meter = meterFactory.Meters.Single(); using var diagnosticsRequestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await middleware.Invoke(httpContext); // Assert Assert.Equal(DiagnosticsMetrics.MeterName, meter.Name); Assert.Null(meter.Version); Assert.Empty(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot()); } [Fact] public async Task Metrics_ExceptionThrown_Handled_Reported() { // Arrange var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor(); var meterFactory = new TestMeterFactory(); var exceptionHandlers = new List<IExceptionHandler> { new TestExceptionHandler(true, "1") }; var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, meterFactory); var meter = meterFactory.Meters.Single(); using var diagnosticsRequestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await middleware.Invoke(httpContext); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), m => AssertRequestException(m, "System.InvalidOperationException", "handled", typeof(TestExceptionHandler).FullName)); } [Fact] public async Task Metrics_ExceptionThrown_ResponseStarted_Skipped() { // Arrange var httpContext = CreateHttpContext(); httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature()); var optionsAccessor = CreateOptionsAccessor(); var meterFactory = new TestMeterFactory(); var exceptionHandlers = new List<IExceptionHandler> { new TestExceptionHandler(true, "1") }; var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, meterFactory); var meter = meterFactory.Meters.Single(); using var diagnosticsRequestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await Assert.ThrowsAsync<InvalidOperationException>(() => middleware.Invoke(httpContext)); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), m => AssertRequestException(m, "System.InvalidOperationException", "skipped")); } private sealed class TestResponseFeature : HttpResponseFeature { public override bool HasStarted => true; } [Fact] public async Task Metrics_ExceptionThrown_DefaultSettings_Handled_Reported() { // Arrange var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor(); var meterFactory = new TestMeterFactory(); var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, meterFactory: meterFactory); var meter = meterFactory.Meters.Single(); using var diagnosticsRequestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await middleware.Invoke(httpContext); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), m => AssertRequestException(m, "System.InvalidOperationException", "handled", null)); } [Fact] public async Task Metrics_ExceptionThrown_Handled_UseOriginalRoute() { // Arrange var originalEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/path"), 0); var originalEndpoint = originalEndpointBuilder.Build(); var meterFactory = new TestMeterFactory(); using var requestDurationCollector = new MetricCollector<double>(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); using var requestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); using var host = new HostBuilder() .ConfigureServices(s => { s.AddSingleton<IMeterFactory>(meterFactory); s.AddSingleton(LoggerFactory); }) .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = (c) => Task.CompletedTask }); app.Run(context => { context.SetEndpoint(originalEndpoint); throw new Exception("Test exception"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); // Act var response = await server.CreateClient().GetAsync("/path"); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); await requestDurationCollector.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); // Assert Assert.Collection( requestDurationCollector.GetMeasurementSnapshot(), m => { Assert.True(m.Value > 0); Assert.Equal(500, (int)m.Tags["http.response.status_code"]); Assert.Equal("System.Exception", (string)m.Tags["error.type"]); Assert.Equal("/path", (string)m.Tags["http.route"]); }); Assert.Collection(requestExceptionCollector.GetMeasurementSnapshot(), m => AssertRequestException(m, "System.Exception", "handled")); } [Fact] public async Task Metrics_ExceptionThrown_Handled_UseNewRoute() { // Arrange var originalEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/path"), 0); var originalEndpoint = originalEndpointBuilder.Build(); var newEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/new"), 0); var newEndpoint = newEndpointBuilder.Build(); var meterFactory = new TestMeterFactory(); using var requestDurationCollector = new MetricCollector<double>(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); using var requestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); using var host = new HostBuilder() .ConfigureServices(s => { s.AddSingleton<IMeterFactory>(meterFactory); s.AddSingleton(LoggerFactory); }) .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = (c) => { c.SetEndpoint(newEndpoint); return Task.CompletedTask; } }); app.Run(context => { context.SetEndpoint(originalEndpoint); throw new Exception("Test exception"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); // Act var response = await server.CreateClient().GetAsync("/path"); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); await requestDurationCollector.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); // Assert Assert.Collection( requestDurationCollector.GetMeasurementSnapshot(), m => { Assert.True(m.Value > 0); Assert.Equal(500, (int)m.Tags["http.response.status_code"]); Assert.Equal("System.Exception", (string)m.Tags["error.type"]); Assert.Equal("/new", (string)m.Tags["http.route"]); }); Assert.Collection(requestExceptionCollector.GetMeasurementSnapshot(), m => AssertRequestException(m, "System.Exception", "handled")); } [Fact] public async Task Metrics_ExceptionThrown_Unhandled_Reported() { // Arrange var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor(exceptionHandler: c => { c.Response.StatusCode = 404; return Task.CompletedTask; }); var meterFactory = new TestMeterFactory(); var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, meterFactory: meterFactory); var meter = meterFactory.Meters.Single(); using var diagnosticsRequestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await Assert.ThrowsAsync<InvalidOperationException>(() => middleware.Invoke(httpContext)); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), m => AssertRequestException(m, "System.InvalidOperationException", "unhandled")); } [Fact] public async Task ExceptionFeatureSetOnDeveloperExceptionPage() { // Arrange var tcs = new TaskCompletionSource<IExceptionHandlerPathFeature>(TaskCreationOptions.RunContinuationsAsynchronously); using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.Use(async (context, next) => { await next(); var exceptionHandlerFeature = context.Features.GetRequiredFeature<IExceptionHandlerPathFeature>(); tcs.SetResult(exceptionHandlerFeature); }); app.UseExceptionHandler(exceptionApp => { exceptionApp.Run(context => Task.CompletedTask); }); app.Run(context => { throw new Exception("Test exception"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); var request = new HttpRequestMessage(HttpMethod.Get, "/path"); var response = await server.CreateClient().SendAsync(request); var feature = await tcs.Task; Assert.NotNull(feature); Assert.Equal("Test exception", feature.Error.Message); Assert.Equal("/path", feature.Path); } [Fact] public async Task Metrics_ExceptionThrown_ErrorPathHandled_Reported() { // Arrange var httpContext = CreateHttpContext(); var optionsAccessor = CreateOptionsAccessor( exceptionHandler: context => { context.Features.Set<IHttpResponseFeature>(new TestHttpResponseFeature()); return Task.CompletedTask; }, exceptionHandlingPath: "/error"); var meterFactory = new TestMeterFactory(); var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, meterFactory: meterFactory); var meter = meterFactory.Meters.Single(); using var diagnosticsRequestExceptionCollector = new MetricCollector<long>(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); // Act await middleware.Invoke(httpContext); // Assert Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(), m => AssertRequestException(m, "System.InvalidOperationException", "handled", "/error")); } private static void AssertRequestException(CollectedMeasurement<long> measurement, string exceptionName, string result, string handler = null) { Assert.Equal(1, measurement.Value); Assert.Equal(exceptionName, (string)measurement.Tags["error.type"]); Assert.Equal(result, measurement.Tags["aspnetcore.diagnostics.exception.result"].ToString()); if (handler == null) { Assert.False(measurement.Tags.ContainsKey("aspnetcore.diagnostics.handler.type")); } else { Assert.Equal(handler, (string)measurement.Tags["aspnetcore.diagnostics.handler.type"]); } } private class TestExceptionHandler : IExceptionHandler { private readonly bool _handle; private readonly string _name; public TestExceptionHandler(bool handle, string name) { _handle = handle; _name = name; } public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { httpContext.Items[_name] = true; return ValueTask.FromResult(_handle); } } private HttpContext CreateHttpContext() { var httpContext = new DefaultHttpContext { RequestServices = new TestServiceProvider() }; return httpContext; } private IOptions<ExceptionHandlerOptions> CreateOptionsAccessor( RequestDelegate exceptionHandler = null, string exceptionHandlingPath = null, Func<ExceptionHandlerSuppressDiagnosticsContext, bool> suppressDiagnosticsCallback = null) { exceptionHandler ??= c => Task.CompletedTask; var options = new ExceptionHandlerOptions() { ExceptionHandler = exceptionHandler, ExceptionHandlingPath = exceptionHandlingPath, }; if (suppressDiagnosticsCallback != null) { options.SuppressDiagnosticsCallback = suppressDiagnosticsCallback; } var optionsAccessor = Mock.Of<IOptions<ExceptionHandlerOptions>>(o => o.Value == options); return optionsAccessor; } private ExceptionHandlerMiddlewareImpl CreateMiddleware( RequestDelegate next, IOptions<ExceptionHandlerOptions> options, IEnumerable<IExceptionHandler> exceptionHandlers = null, IMeterFactory meterFactory = null, ILoggerFactory loggerFactory = null) { next ??= c => Task.CompletedTask; var listener = new DiagnosticListener("Microsoft.AspNetCore"); var middleware = new ExceptionHandlerMiddlewareImpl( next, loggerFactory ?? NullLoggerFactory.Instance, options, listener, exceptionHandlers ?? Enumerable.Empty<IExceptionHandler>(), meterFactory ?? new TestMeterFactory()); return middleware; } private class TestServiceProvider : IServiceProvider { public object GetService(Type serviceType) { throw new NotImplementedException(); } } private sealed class TestHttpMetricsTagsFeature : IHttpMetricsTagsFeature { public List<KeyValuePair<string, object>> TagsList { get; } = new List<KeyValuePair<string, object>>(); public ICollection<KeyValuePair<string, object>> Tags => TagsList; public bool MetricsDisabled { get; set; } } }