diff --git a/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj b/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj index e95eda050f..eeb85ae239 100644 --- a/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj +++ b/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj @@ -5,6 +5,6 @@ - + diff --git a/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.netcoreapp3.0.cs b/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.netcoreapp3.0.cs index 8ec0333ac7..9d8c6215ca 100644 --- a/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.netcoreapp3.0.cs +++ b/src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.netcoreapp3.0.cs @@ -24,10 +24,20 @@ namespace Microsoft.AspNetCore.Diagnostics public int StartColumn { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public int StartLine { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } + public partial class ErrorContext + { + public ErrorContext(Microsoft.AspNetCore.Http.HttpContext httpContext, System.Exception exception) { } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.AspNetCore.Http.HttpContext HttpContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } public partial interface ICompilationException { System.Collections.Generic.IEnumerable CompilationFailures { get; } } + public partial interface IDeveloperPageExceptionFilter + { + System.Threading.Tasks.Task HandleExceptionAsync(Microsoft.AspNetCore.Diagnostics.ErrorContext errorContext, System.Func next); + } public partial interface IExceptionHandlerFeature { System.Exception Error { get; } diff --git a/src/Middleware/Diagnostics.Abstractions/src/ErrorContext.cs b/src/Middleware/Diagnostics.Abstractions/src/ErrorContext.cs new file mode 100644 index 0000000000..5c4f973f5d --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/ErrorContext.cs @@ -0,0 +1,35 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// Provides context about the error currently being handled bt the DeveloperExceptionPageMiddleware. + /// + public class ErrorContext + { + /// + /// Initializes the ErrorContext with the specified and . + /// + /// + /// + public ErrorContext(HttpContext httpContext, Exception exception) + { + HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + } + + /// + /// The . + /// + public HttpContext HttpContext { get; } + + /// + /// The thrown during request processing. + /// + public Exception Exception { get; } + } +} diff --git a/src/Middleware/Diagnostics.Abstractions/src/IDeveloperPageExceptionFilter.cs b/src/Middleware/Diagnostics.Abstractions/src/IDeveloperPageExceptionFilter.cs new file mode 100644 index 0000000000..e908f48006 --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/IDeveloperPageExceptionFilter.cs @@ -0,0 +1,24 @@ +// 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.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// Provides an extensiblity point for changing the behavior of the DeveloperExceptionPageMiddleware. + /// + public interface IDeveloperPageExceptionFilter + { + /// + /// An exception handling method that is used to either format the exception or delegate to the next handler in the chain. + /// + /// The error context. + /// The next filter in the pipeline. + /// A task the completes when the handler is done executing. + Task HandleExceptionAsync(ErrorContext errorContext, Func next); + } +} diff --git a/src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj b/src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj index 94a61d84d0..d7f67d031c 100644 --- a/src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj +++ b/src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core diagnostics middleware abstractions and feature interface definitions. @@ -9,4 +9,8 @@ aspnetcore;diagnostics + + + + diff --git a/src/Middleware/Diagnostics/ref/Microsoft.AspNetCore.Diagnostics.netcoreapp3.0.cs b/src/Middleware/Diagnostics/ref/Microsoft.AspNetCore.Diagnostics.netcoreapp3.0.cs index d0b332177b..3f6402c5d8 100644 --- a/src/Middleware/Diagnostics/ref/Microsoft.AspNetCore.Diagnostics.netcoreapp3.0.cs +++ b/src/Middleware/Diagnostics/ref/Microsoft.AspNetCore.Diagnostics.netcoreapp3.0.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Diagnostics { public partial class DeveloperExceptionPageMiddleware { - public DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment, System.Diagnostics.DiagnosticSource diagnosticSource) { } + public DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment, System.Diagnostics.DiagnosticSource diagnosticSource, System.Collections.Generic.IEnumerable filters) { } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; } } diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index ea568010ca..22c7c39b47 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Diagnostics private readonly IFileProvider _fileProvider; private readonly DiagnosticSource _diagnosticSource; private readonly ExceptionDetailsProvider _exceptionDetailsProvider; + private readonly Func _exceptionHandler; /// /// Initializes a new instance of the class @@ -40,12 +41,14 @@ namespace Microsoft.AspNetCore.Diagnostics /// /// /// + /// public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions options, ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment, - DiagnosticSource diagnosticSource) + DiagnosticSource diagnosticSource, + IEnumerable filters) { if (next == null) { @@ -57,12 +60,24 @@ namespace Microsoft.AspNetCore.Diagnostics throw new ArgumentNullException(nameof(options)); } + if (filters == null) + { + throw new ArgumentNullException(nameof(filters)); + } + _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger(); _fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider; _diagnosticSource = diagnosticSource; _exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _options.SourceCodeLineCount); + _exceptionHandler = DisplayException; + + foreach (var filter in filters.Reverse()) + { + var nextFilter = _exceptionHandler; + _exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter); + } } /// @@ -91,7 +106,7 @@ namespace Microsoft.AspNetCore.Diagnostics context.Response.Clear(); context.Response.StatusCode = 500; - await DisplayException(context, ex); + await _exceptionHandler(new ErrorContext(context, ex)); if (_diagnosticSource.IsEnabled("Microsoft.AspNetCore.Diagnostics.UnhandledException")) { @@ -110,15 +125,15 @@ namespace Microsoft.AspNetCore.Diagnostics } // Assumes the response headers have not been sent. If they have, still attempt to write to the body. - private Task DisplayException(HttpContext context, Exception ex) + private Task DisplayException(ErrorContext errorContext) { - var compilationException = ex as ICompilationException; + var compilationException = errorContext.Exception as ICompilationException; if (compilationException != null) { - return DisplayCompilationException(context, compilationException); + return DisplayCompilationException(errorContext.HttpContext, compilationException); } - return DisplayRuntimeException(context, ex); + return DisplayRuntimeException(errorContext.HttpContext, errorContext.Exception); } private Task DisplayCompilationException( @@ -215,7 +230,7 @@ namespace Microsoft.AspNetCore.Diagnostics } } } - + var request = context.Request; var model = new ErrorPageModel diff --git a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs index 2d0f6b42db..8552a2da8e 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs @@ -3,12 +3,11 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -46,6 +45,88 @@ namespace Microsoft.AspNetCore.Diagnostics Assert.Null(listener.DiagnosticHandledException?.Exception); } + [Fact] + public async Task ExceptionPageFiltersAreApplied() + { + // Arrange + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .Configure(app => + { + app.UseDeveloperExceptionPage(); + app.Run(context => + { + throw new Exception("Test exception"); + }); + }); + var server = new TestServer(builder); + + // Act + var response = await server.CreateClient().GetAsync("/path"); + + // Assert + Assert.Equal("Test exception", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ExceptionFilterCallingNextWorks() + { + // Arrange + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }) + .Configure(app => + { + app.UseDeveloperExceptionPage(); + app.Run(context => + { + throw new Exception("Test exception"); + }); + }); + var server = new TestServer(builder); + + // Act + var response = await server.CreateClient().GetAsync("/path"); + + // Assert + Assert.Equal("Bad format exception!", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ExceptionPageFiltersAreAppliedInOrder() + { + // Arrange + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }) + .Configure(app => + { + app.UseDeveloperExceptionPage(); + app.Run(context => + { + throw new Exception("Test exception"); + }); + }); + var server = new TestServer(builder); + + // Act + var response = await server.CreateClient().GetAsync("/path"); + + // Assert + Assert.Equal("An error occurred", await response.Content.ReadAsStringAsync()); + } + public static TheoryData CompilationExceptionData { get @@ -140,5 +221,45 @@ namespace Microsoft.AspNetCore.Diagnostics public IEnumerable CompilationFailures { get; } } + + public class ExceptionMessageFilter : IDeveloperPageExceptionFilter + { + public Task HandleExceptionAsync(ErrorContext context, Func next) + { + return context.HttpContext.Response.WriteAsync(context.Exception.Message); + } + } + + public class ExceptionToStringFilter : IDeveloperPageExceptionFilter + { + public Task HandleExceptionAsync(ErrorContext context, Func next) + { + return context.HttpContext.Response.WriteAsync(context.Exception.ToString()); + } + } + + public class AlwaysThrowSameMessageFilter : IDeveloperPageExceptionFilter + { + public Task HandleExceptionAsync(ErrorContext context, Func next) + { + return context.HttpContext.Response.WriteAsync("An error occurred"); + } + } + + public class AlwaysBadFormatExceptionFilter : IDeveloperPageExceptionFilter + { + public Task HandleExceptionAsync(ErrorContext context, Func next) + { + return next(new ErrorContext(context.HttpContext, new FormatException("Bad format exception!"))); + } + } + + public class PassThroughExceptionFilter : IDeveloperPageExceptionFilter + { + public Task HandleExceptionAsync(ErrorContext context, Func next) + { + return next(context); + } + } } }