Initial design for exception page filters (#8958)
- This change introduces the concept of an IDeveloperPageException filter that runs whenever the developer exception page has encountered an error. It follows the middleware pattern (chain of resposibility) which allows short circuiting or decorating the default logic. - Added tests
This commit is contained in:
parent
42b3fada31
commit
f4c61de490
|
|
@ -5,6 +5,6 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
|
||||
<Compile Include="Microsoft.AspNetCore.Diagnostics.Abstractions.netcoreapp3.0.cs" />
|
||||
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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<Microsoft.AspNetCore.Diagnostics.CompilationFailure> CompilationFailures { get; }
|
||||
}
|
||||
public partial interface IDeveloperPageExceptionFilter
|
||||
{
|
||||
System.Threading.Tasks.Task HandleExceptionAsync(Microsoft.AspNetCore.Diagnostics.ErrorContext errorContext, System.Func<Microsoft.AspNetCore.Diagnostics.ErrorContext, System.Threading.Tasks.Task> next);
|
||||
}
|
||||
public partial interface IExceptionHandlerFeature
|
||||
{
|
||||
System.Exception Error { get; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides context about the error currently being handled bt the DeveloperExceptionPageMiddleware.
|
||||
/// </summary>
|
||||
public class ErrorContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the ErrorContext with the specified <see cref="HttpContext"/> and <see cref="Exception"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <param name="exception"></param>
|
||||
public ErrorContext(HttpContext httpContext, Exception exception)
|
||||
{
|
||||
HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
|
||||
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HttpContext"/>.
|
||||
/// </summary>
|
||||
public HttpContext HttpContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Exception"/> thrown during request processing.
|
||||
/// </summary>
|
||||
public Exception Exception { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an extensiblity point for changing the behavior of the DeveloperExceptionPageMiddleware.
|
||||
/// </summary>
|
||||
public interface IDeveloperPageExceptionFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception handling method that is used to either format the exception or delegate to the next handler in the chain.
|
||||
/// </summary>
|
||||
/// <param name="errorContext">The error context.</param>
|
||||
/// <param name="next">The next filter in the pipeline.</param>
|
||||
/// <returns>A task the completes when the handler is done executing.</returns>
|
||||
Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core diagnostics middleware abstractions and feature interface definitions.</Description>
|
||||
|
|
@ -9,4 +9,8 @@
|
|||
<PackageTags>aspnetcore;diagnostics</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Diagnostics
|
|||
{
|
||||
public partial class DeveloperExceptionPageMiddleware
|
||||
{
|
||||
public DeveloperExceptionPageMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Builder.DeveloperExceptionPageOptions> 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<Microsoft.AspNetCore.Builder.DeveloperExceptionPageOptions> options, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment, System.Diagnostics.DiagnosticSource diagnosticSource, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Diagnostics.IDeveloperPageExceptionFilter> filters) { }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Diagnostics
|
|||
private readonly IFileProvider _fileProvider;
|
||||
private readonly DiagnosticSource _diagnosticSource;
|
||||
private readonly ExceptionDetailsProvider _exceptionDetailsProvider;
|
||||
private readonly Func<ErrorContext, Task> _exceptionHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeveloperExceptionPageMiddleware"/> class
|
||||
|
|
@ -40,12 +41,14 @@ namespace Microsoft.AspNetCore.Diagnostics
|
|||
/// <param name="loggerFactory"></param>
|
||||
/// <param name="hostingEnvironment"></param>
|
||||
/// <param name="diagnosticSource"></param>
|
||||
/// <param name="filters"></param>
|
||||
public DeveloperExceptionPageMiddleware(
|
||||
RequestDelegate next,
|
||||
IOptions<DeveloperExceptionPageOptions> options,
|
||||
ILoggerFactory loggerFactory,
|
||||
IWebHostEnvironment hostingEnvironment,
|
||||
DiagnosticSource diagnosticSource)
|
||||
DiagnosticSource diagnosticSource,
|
||||
IEnumerable<IDeveloperPageExceptionFilter> 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<DeveloperExceptionPageMiddleware>();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<IDeveloperPageExceptionFilter, ExceptionMessageFilter>();
|
||||
})
|
||||
.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<IDeveloperPageExceptionFilter, PassThroughExceptionFilter>();
|
||||
services.AddSingleton<IDeveloperPageExceptionFilter, AlwaysBadFormatExceptionFilter>();
|
||||
services.AddSingleton<IDeveloperPageExceptionFilter, ExceptionMessageFilter>();
|
||||
})
|
||||
.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<IDeveloperPageExceptionFilter, AlwaysThrowSameMessageFilter>();
|
||||
services.AddSingleton<IDeveloperPageExceptionFilter, ExceptionMessageFilter>();
|
||||
services.AddSingleton<IDeveloperPageExceptionFilter, ExceptionToStringFilter>();
|
||||
})
|
||||
.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<CompilationFailure> CompilationFailures { get; }
|
||||
}
|
||||
|
||||
public class ExceptionMessageFilter : IDeveloperPageExceptionFilter
|
||||
{
|
||||
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
|
||||
{
|
||||
return context.HttpContext.Response.WriteAsync(context.Exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class ExceptionToStringFilter : IDeveloperPageExceptionFilter
|
||||
{
|
||||
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
|
||||
{
|
||||
return context.HttpContext.Response.WriteAsync(context.Exception.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public class AlwaysThrowSameMessageFilter : IDeveloperPageExceptionFilter
|
||||
{
|
||||
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
|
||||
{
|
||||
return context.HttpContext.Response.WriteAsync("An error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
public class AlwaysBadFormatExceptionFilter : IDeveloperPageExceptionFilter
|
||||
{
|
||||
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
|
||||
{
|
||||
return next(new ErrorContext(context.HttpContext, new FormatException("Bad format exception!")));
|
||||
}
|
||||
}
|
||||
|
||||
public class PassThroughExceptionFilter : IDeveloperPageExceptionFilter
|
||||
{
|
||||
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
|
||||
{
|
||||
return next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue