/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs
1 175 строк
43 KB
Lars
Add an option on the `ExceptionHandlerOptions` to select status codes based on exceptions (#56616)
22 июл 2024, 21:21
Не верифицирован
22 июл 2024, 21:21
137d04d
Код
Авторство
О чём код?
// 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using System.Net.Http; namespace Microsoft.AspNetCore.Diagnostics; public class ExceptionHandlerTest { [Theory] [InlineData(HttpStatusCode.NotFound)] [InlineData(HttpStatusCode.BadRequest)] [InlineData(HttpStatusCode.InternalServerError)] public async Task OnlyHandles_UnhandledExceptions(HttpStatusCode expectedStatusCode) { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { await httpContext.Response.WriteAsync("Handled error in a custom way."); }); }); app.Run((RequestDelegate)(async (context) => { context.Response.StatusCode = (int)expectedStatusCode; context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync("An error occurred while adding a product"); })); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); Assert.Equal(expectedStatusCode, response.StatusCode); Assert.Equal("An error occurred while adding a product", await response.Content.ReadAsStringAsync()); } } [Fact] public async Task DoesNotHandle_UnhandledExceptions_WhenResponseAlreadyStarted() { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; } Assert.NotNull(exception); Assert.Equal("Something bad happened", exception.Message); }); app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { await httpContext.Response.WriteAsync("Handled error in a custom way."); }); }); app.Run(async (httpContext) => { await httpContext.Response.WriteAsync("Hello"); throw new InvalidOperationException("Something bad happened"); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); response.EnsureSuccessStatusCode(); Assert.Equal("Hello", await response.Content.ReadAsStringAsync()); } } [Fact] public async Task ClearsResponseBuffer_BeforeRequestIsReexecuted() { var expectedResponseBody = "New response body"; using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { // add response buffering app.Use(async (httpContext, next) => { var response = httpContext.Response; var originalResponseBody = response.Body; var bufferingStream = new MemoryStream(); response.Body = bufferingStream; try { await next(httpContext); } finally { response.Body = originalResponseBody; } bufferingStream.Seek(0, SeekOrigin.Begin); await bufferingStream.CopyToAsync(response.Body); }); app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { Assert.True(httpContext.Response.Body.CanSeek); Assert.Equal(0, httpContext.Response.Body.Position); await httpContext.Response.WriteAsync(expectedResponseBody); }); }); app.Run(async (context) => { // Write some content into the response before throwing exception await context.Response.WriteAsync(new string('a', 100)); throw new InvalidOperationException("Invalid input provided."); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); Assert.Equal(expectedResponseBody, await response.Content.ReadAsStringAsync()); IEnumerable<string> values; Assert.True(response.Headers.CacheControl.NoCache); Assert.True(response.Headers.CacheControl.NoStore); Assert.True(response.Headers.TryGetValues("Pragma", out values)); Assert.Single(values); Assert.Equal("no-cache", values.First()); Assert.True(response.Content.Headers.TryGetValues("Expires", out values)); Assert.Single(values); Assert.Equal("-1", values.First()); Assert.False(response.Headers.TryGetValues("ETag", out values)); } } [Fact] public async Task ClearsCacheHeaders_SetByReexecutionPathHandlers() { var expiresTime = DateTime.UtcNow.AddDays(5).ToString("R"); var expectedResponseBody = "Handled error in a custom way."; using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { httpContext.Response.Headers.Add("Cache-Control", new[] { "max-age=600" }); httpContext.Response.Headers.Add("Pragma", new[] { "max-age=600" }); httpContext.Response.Headers.Add( "Expires", new[] { expiresTime }); httpContext.Response.Headers.Add("ETag", new[] { "12345" }); await httpContext.Response.WriteAsync(expectedResponseBody); }); }); app.Run((context) => { throw new InvalidOperationException("Invalid input provided."); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); Assert.Equal(expectedResponseBody, await response.Content.ReadAsStringAsync()); IEnumerable<string> values; Assert.True(response.Headers.CacheControl.NoCache); Assert.True(response.Headers.CacheControl.NoStore); Assert.True(response.Headers.TryGetValues("Pragma", out values)); Assert.Single(values); Assert.Equal("no-cache", values.First()); Assert.True(response.Content.Headers.TryGetValues("Expires", out values)); Assert.Single(values); Assert.Equal("-1", values.First()); Assert.False(response.Headers.TryGetValues("ETag", out values)); } } [Fact] public async Task DoesNotModifyCacheHeaders_WhenNoExceptionIsThrown() { var expiresTime = DateTime.UtcNow.AddDays(10).ToString("R"); var expectedResponseBody = "Hello world!"; using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { await httpContext.Response.WriteAsync("Handled error in a custom way."); }); }); app.Run(async (httpContext) => { httpContext.Response.Headers.Add("Cache-Control", new[] { "max-age=3600" }); httpContext.Response.Headers.Add("Pragma", new[] { "max-age=3600" }); httpContext.Response.Headers.Add("Expires", new[] { expiresTime }); httpContext.Response.Headers.Add("ETag", new[] { "abcdef" }); await httpContext.Response.WriteAsync(expectedResponseBody); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); response.EnsureSuccessStatusCode(); Assert.Equal(expectedResponseBody, await response.Content.ReadAsStringAsync()); IEnumerable<string> values; Assert.True(response.Headers.TryGetValues("Cache-Control", out values)); Assert.Single(values); Assert.Equal("max-age=3600", values.First()); Assert.True(response.Headers.TryGetValues("Pragma", out values)); Assert.Single(values); Assert.Equal("max-age=3600", values.First()); Assert.True(response.Content.Headers.TryGetValues("Expires", out values)); Assert.Single(values); Assert.Equal(expiresTime, values.First()); Assert.True(response.Headers.TryGetValues("ETag", out values)); Assert.Single(values); Assert.Equal("abcdef", values.First()); } } [Fact] public async Task ExceptionHandlerSucceeded_IfExceptionHandlerResponseHasStarted() { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; } Assert.Null(exception); }); app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { httpContext.Response.StatusCode = StatusCodes.Status404NotFound; await httpContext.Response.WriteAsync("Custom 404"); }); }); app.Run(httpContext => { httpContext.Response.Headers.Add("Cache-Control", new[] { "max-age=3600" }); httpContext.Response.Headers.Add("Pragma", new[] { "max-age=3600" }); httpContext.Response.Headers.Add("Expires", new[] { DateTime.UtcNow.AddDays(10).ToString("R") }); httpContext.Response.Headers.Add("ETag", new[] { "abcdef" }); throw new InvalidOperationException("Something bad happened"); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Equal("Custom 404", await response.Content.ReadAsStringAsync()); IEnumerable<string> values; Assert.True(response.Headers.CacheControl.NoCache); Assert.True(response.Headers.CacheControl.NoStore); Assert.True(response.Headers.TryGetValues("Pragma", out values)); Assert.Single(values); Assert.Equal("no-cache", values.First()); Assert.False(response.Headers.TryGetValues("Expires", out _)); Assert.False(response.Headers.TryGetValues("ETag", out _)); } } [Fact] public async Task DoesNotClearCacheHeaders_WhenResponseHasAlreadyStarted() { var expiresTime = DateTime.UtcNow.AddDays(10).ToString("R"); using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; } Assert.NotNull(exception); Assert.Equal("Something bad happened", exception.Message); }); app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { await httpContext.Response.WriteAsync("Handled error in a custom way."); }); }); app.Run(async (httpContext) => { httpContext.Response.Headers.Add("Cache-Control", new[] { "max-age=3600" }); httpContext.Response.Headers.Add("Pragma", new[] { "max-age=3600" }); httpContext.Response.Headers.Add("Expires", new[] { expiresTime }); httpContext.Response.Headers.Add("ETag", new[] { "abcdef" }); await httpContext.Response.WriteAsync("Hello"); throw new InvalidOperationException("Something bad happened"); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); response.EnsureSuccessStatusCode(); Assert.Equal("Hello", await response.Content.ReadAsStringAsync()); IEnumerable<string> values; Assert.True(response.Headers.TryGetValues("Cache-Control", out values)); Assert.Single(values); Assert.Equal("max-age=3600", values.First()); Assert.True(response.Headers.TryGetValues("Pragma", out values)); Assert.Single(values); Assert.Equal("max-age=3600", values.First()); Assert.True(response.Content.Headers.TryGetValues("Expires", out values)); Assert.Single(values); Assert.Equal(expiresTime, values.First()); Assert.True(response.Headers.TryGetValues("ETag", out values)); Assert.Single(values); Assert.Equal("abcdef", values.First()); } } [Fact] public async Task HandledErrorsWriteToDiagnosticWhenUsingExceptionHandler() { // Arrange DiagnosticListener diagnosticListener = null; using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { diagnosticListener = app.ApplicationServices.GetRequiredService<DiagnosticListener>(); app.UseExceptionHandler("/handle-errors"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { await httpContext.Response.WriteAsync("Handled error in a custom way."); }); }); app.Run(context => { throw new Exception("Test exception"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); var listener = new TestDiagnosticListener(); diagnosticListener.SubscribeWithAdapter(listener); // Act await server.CreateClient().GetAsync(string.Empty); // This ensures that all diagnostics are completely written to the diagnostic listener Thread.Sleep(1000); // Assert Assert.NotNull(listener.EndRequest?.HttpContext); Assert.Null(listener.HostingUnhandledException?.HttpContext); Assert.Null(listener.HostingUnhandledException?.Exception); Assert.Null(listener.DiagnosticUnhandledException?.HttpContext); Assert.Null(listener.DiagnosticUnhandledException?.Exception); Assert.NotNull(listener.DiagnosticHandledException?.HttpContext); Assert.NotNull(listener.DiagnosticHandledException?.Exception); } [Fact] public void UsingExceptionHandler_ThrowsAnException_WhenExceptionHandlingPathNotSet() { // Arrange DiagnosticListener diagnosticListener = null; using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { diagnosticListener = app.ApplicationServices.GetRequiredService<DiagnosticListener>(); app.UseExceptionHandler(); }); }).Build(); // Act var exception = Assert.Throws<InvalidOperationException>(() => host.Start()); // Assert Assert.Equal("An error occurred when configuring the exception handler middleware. " + "Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'. " + "Alternatively, set one of the aforementioned properties in 'Startup.ConfigureServices' as follows: 'services.AddExceptionHandler(options => { ... });' " + "or configure to generate a 'ProblemDetails' response in 'service.AddProblemDetails()'.", exception.Message); } [Fact] public async Task ExceptionHandlerNotFound_ThrowsIOEWithOriginalError() { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; // This mimics what the server would do when an exception occurs httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; } // Invalid operation exception Assert.NotNull(exception); Assert.Equal("The exception handler configured on ExceptionHandlerOptions produced a 404 status response. " + "This InvalidOperationException containing the original exception was thrown since this is often due to a misconfigured ExceptionHandlingPath. " + "If the exception handler is expected to return 404 status responses then set AllowStatusCode404Response to true.", exception.Message); // The original exception is inner exception Assert.NotNull(exception.InnerException); Assert.IsType<ApplicationException>(exception.InnerException); Assert.Equal("Something bad happened.", exception.InnerException.Message); }); app.UseExceptionHandler("/non-existent-hander"); app.Map("/handle-errors", (innerAppBuilder) => { innerAppBuilder.Run(async (httpContext) => { await httpContext.Response.WriteAsync("Handled error in a custom way."); }); }); app.Map("/throw", (innerAppBuilder) => { innerAppBuilder.Run(httpContext => { throw new ApplicationException("Something bad happened."); }); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync("throw"); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); } } [Fact] public async Task ExceptionHandler_CanReturn404Responses_WhenAllowed() { var sink = new TestSink(TestSink.EnableWithTypeName<ExceptionHandlerMiddleware>); var loggerFactory = new TestLoggerFactory(sink, enabled: true); using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .ConfigureServices(services => { services.AddSingleton<ILoggerFactory>(loggerFactory); services.Configure<ExceptionHandlerOptions>(options => { options.AllowStatusCode404Response = true; options.ExceptionHandler = httpContext => { httpContext.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; }); }) .Configure(app => { app.UseExceptionHandler(); app.Map("/throw", (innerAppBuilder) => { innerAppBuilder.Run(httpContext => { throw new InvalidOperationException("Something bad happened."); }); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync("throw"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); } Assert.DoesNotContain(sink.Writes, w => w.LogLevel == LogLevel.Warning && w.EventId == 4 && w.Message == "No exception handler was found, rethrowing original exception."); } [Fact] public async Task ExceptionHandler_SelectsStatusCode() { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .ConfigureServices(services => services.AddProblemDetails()) .Configure(app => { app.UseExceptionHandler(new ExceptionHandlerOptions { StatusCodeSelector = ex => ex is ApplicationException ? StatusCodes.Status409Conflict : StatusCodes.Status500InternalServerError, }); app.Map("/throw", innerAppBuilder => { innerAppBuilder.Run(_ => throw new ApplicationException("Something bad happened.")); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync("throw"); Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); } } [Fact] public async Task StatusCodeSelector_CanSelect404() { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .ConfigureServices(services => services.AddProblemDetails()) .Configure(app => { app.UseExceptionHandler(new ExceptionHandlerOptions { // 404 is not allowed, // but as the exception is explicitly mapped to 404 by the StatusCodeSelector, // it should be set anyway. AllowStatusCode404Response = false, StatusCodeSelector = ex => ex is ApplicationException ? StatusCodes.Status404NotFound : StatusCodes.Status500InternalServerError, }); app.Map("/throw", innerAppBuilder => { innerAppBuilder.Run(_ => throw new ApplicationException("Something bad happened.")); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync("throw"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } [Fact] public async Task ExceptionHandlerWithOwnBuilder() { var sink = new TestSink(TestSink.EnableWithTypeName<ExceptionHandlerMiddleware>); using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler(builder => { builder.Run(c => { c.Response.StatusCode = 200; return c.Response.WriteAsync("separate pipeline"); }); }); app.Map("/throw", (innerAppBuilder) => { innerAppBuilder.Run(httpContext => { throw new InvalidOperationException("Something bad happened."); }); }); }); }).Build(); await host.StartAsync(); using (var server = host.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync("throw"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("separate pipeline", await response.Content.ReadAsStringAsync()); } } [Fact] public async Task ExceptionHandlerWithPathWorksAfterUseRoutingIfGlobalRouteBuilderUsed() { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; } Assert.Null(exception); }); app.UseRouting(); app.UseExceptionHandler("/handle-errors"); app.UseEndpoints(endpoints => { endpoints.Map("/handle-errors", c => { c.Response.StatusCode = 200; return c.Response.WriteAsync("Handled"); }); }); app.Run((httpContext) => { throw new InvalidOperationException("Something bad happened"); }); await app.StartAsync(); using (var server = app.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); response.EnsureSuccessStatusCode(); Assert.Equal("Handled", await response.Content.ReadAsStringAsync()); } } [Fact] public async Task ExceptionHandlerWithOptionsWorksAfterUseRoutingIfGlobalRouteBuilderUsed() { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; } Assert.Null(exception); }); app.UseRouting(); app.UseExceptionHandler(new ExceptionHandlerOptions() { ExceptionHandlingPath = "/handle-errors" }); app.UseEndpoints(endpoints => { endpoints.Map("/handle-errors", c => { c.Response.StatusCode = 200; return c.Response.WriteAsync("Handled"); }); }); app.Run((httpContext) => { throw new InvalidOperationException("Something bad happened"); }); await app.StartAsync(); using (var server = app.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); response.EnsureSuccessStatusCode(); Assert.Equal("Handled", await response.Content.ReadAsStringAsync()); } } [Fact] public async Task ExceptionHandlerWithAddWorksAfterUseRoutingIfGlobalRouteBuilderUsed() { var builder = WebApplication.CreateBuilder(); builder.Services.AddExceptionHandler(o => o.ExceptionHandlingPath = "/handle-errors"); builder.WebHost.UseTestServer(); await using var app = builder.Build(); app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; } Assert.Null(exception); }); app.UseRouting(); app.UseExceptionHandler(); app.UseEndpoints(endpoints => { endpoints.Map("/handle-errors", c => { c.Response.StatusCode = 200; return c.Response.WriteAsync("Handled"); }); }); app.Run((httpContext) => { throw new InvalidOperationException("Something bad happened"); }); await app.StartAsync(); using (var server = app.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); response.EnsureSuccessStatusCode(); Assert.Equal("Handled", await response.Content.ReadAsStringAsync()); } } [Fact] public async Task ExceptionHandlerWithExceptionHandlerNotReplacedWithGlobalRouteBuilder() { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); await using var app = builder.Build(); app.Use(async (httpContext, next) => { Exception exception = null; try { await next(httpContext); } catch (InvalidOperationException ex) { exception = ex; } Assert.Null(exception); }); app.UseRouting(); app.UseExceptionHandler(new ExceptionHandlerOptions() { ExceptionHandler = httpContext => { httpContext.Response.StatusCode = StatusCodes.Status404NotFound; return httpContext.Response.WriteAsync("Custom handler"); } }); app.UseEndpoints(endpoints => { endpoints.Map("/handle-errors", c => { c.Response.StatusCode = 200; return c.Response.WriteAsync("Handled"); }); }); app.Run((httpContext) => { throw new InvalidOperationException("Something bad happened"); }); await app.StartAsync(); using (var server = app.GetTestServer()) { var client = server.CreateClient(); var response = await client.GetAsync(string.Empty); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Equal("Custom handler", await response.Content.ReadAsStringAsync()); } } [Fact] public async Task UnhandledError_ExceptionNameTagAdded() { // Arrange var meterFactory = new TestMeterFactory(); using var instrumentCollector = new MetricCollector<double>(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); using var host = new HostBuilder() .ConfigureServices(s => { s.AddSingleton<IMeterFactory>(meterFactory); }) .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { app.UseExceptionHandler(new ExceptionHandlerOptions() { ExceptionHandler = httpContext => { httpContext.Response.StatusCode = StatusCodes.Status404NotFound; return httpContext.Response.WriteAsync("Custom handler"); } }); app.Run(context => { throw new Exception("Test exception"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); // Act var response = await server.CreateClient().GetAsync("/path"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); await instrumentCollector.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); // Assert Assert.Collection( instrumentCollector.GetMeasurementSnapshot(), m => { Assert.True(m.Value > 0); Assert.Equal(404, (int)m.Tags["http.response.status_code"]); Assert.Equal("System.Exception", (string)m.Tags["error.type"]); }); } [Fact] public async Task UnhandledError_MultipleHandlers_ExceptionNameTagAddedOnce() { // Arrange var meterFactory = new TestMeterFactory(); using var instrumentCollector = new MetricCollector<double>(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); using var host = new HostBuilder() .ConfigureServices(s => { s.AddSingleton<IMeterFactory>(meterFactory); }) .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { // Second error and handler app.UseExceptionHandler(new ExceptionHandlerOptions() { ExceptionHandler = httpContext => { httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; return Task.CompletedTask; } }); app.Use(async (context, next) => { await next(); throw new InvalidOperationException("Test exception2"); }); // First error and handler app.UseExceptionHandler(new ExceptionHandlerOptions() { ExceptionHandler = httpContext => { httpContext.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; } }); app.Run(context => { throw new Exception("Test exception1"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); // Act var response = await server.CreateClient().GetAsync("/path"); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); await instrumentCollector.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); // Assert Assert.Collection( instrumentCollector.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"]); }); } [Fact] public async Task UnhandledError_ErrorAfterHandler_ExceptionNameTagAddedOnce() { // Arrange var meterFactory = new TestMeterFactory(); using var instrumentCollector = new MetricCollector<double>(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); using var host = new HostBuilder() .ConfigureServices(s => { s.AddSingleton<IMeterFactory>(meterFactory); }) .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseTestServer() .Configure(app => { // Second error app.Use(async (context, next) => { await next(); throw new InvalidOperationException("Test exception2"); }); // First error and handler app.UseExceptionHandler(new ExceptionHandlerOptions() { ExceptionHandler = httpContext => { httpContext.Response.StatusCode = StatusCodes.Status404NotFound; return httpContext.Response.WriteAsync("Custom handler"); } }); app.Run(context => { throw new Exception("Test exception1"); }); }); }).Build(); await host.StartAsync(); var server = host.GetTestServer(); // Act await Assert.ThrowsAsync<HttpRequestException>(async () => await server.CreateClient().GetAsync("/path")); await instrumentCollector.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); // Assert Assert.Collection( instrumentCollector.GetMeasurementSnapshot(), m => { Assert.True(m.Value > 0); Assert.Equal(404, (int)m.Tags["http.response.status_code"]); Assert.Equal("System.Exception", (string)m.Tags["error.type"]); }); } }