Skip to content

Commit f4c61de

Browse files
authored
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
1 parent 42b3fad commit f4c61de

8 files changed

+221
-12
lines changed

src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
</PropertyGroup>
66
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
77
<Compile Include="Microsoft.AspNetCore.Diagnostics.Abstractions.netcoreapp3.0.cs" />
8-
8+
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
99
</ItemGroup>
1010
</Project>

src/Middleware/Diagnostics.Abstractions/ref/Microsoft.AspNetCore.Diagnostics.Abstractions.netcoreapp3.0.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,20 @@ public DiagnosticMessage(string message, string formattedMessage, string filePat
2424
public int StartColumn { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
2525
public int StartLine { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
2626
}
27+
public partial class ErrorContext
28+
{
29+
public ErrorContext(Microsoft.AspNetCore.Http.HttpContext httpContext, System.Exception exception) { }
30+
public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
31+
public Microsoft.AspNetCore.Http.HttpContext HttpContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
32+
}
2733
public partial interface ICompilationException
2834
{
2935
System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Diagnostics.CompilationFailure> CompilationFailures { get; }
3036
}
37+
public partial interface IDeveloperPageExceptionFilter
38+
{
39+
System.Threading.Tasks.Task HandleExceptionAsync(Microsoft.AspNetCore.Diagnostics.ErrorContext errorContext, System.Func<Microsoft.AspNetCore.Diagnostics.ErrorContext, System.Threading.Tasks.Task> next);
40+
}
3141
public partial interface IExceptionHandlerFeature
3242
{
3343
System.Exception Error { get; }
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Diagnostics
8+
{
9+
/// <summary>
10+
/// Provides context about the error currently being handled bt the DeveloperExceptionPageMiddleware.
11+
/// </summary>
12+
public class ErrorContext
13+
{
14+
/// <summary>
15+
/// Initializes the ErrorContext with the specified <see cref="HttpContext"/> and <see cref="Exception"/>.
16+
/// </summary>
17+
/// <param name="httpContext"></param>
18+
/// <param name="exception"></param>
19+
public ErrorContext(HttpContext httpContext, Exception exception)
20+
{
21+
HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
22+
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
23+
}
24+
25+
/// <summary>
26+
/// The <see cref="HttpContext"/>.
27+
/// </summary>
28+
public HttpContext HttpContext { get; }
29+
30+
/// <summary>
31+
/// The <see cref="Exception"/> thrown during request processing.
32+
/// </summary>
33+
public Exception Exception { get; }
34+
}
35+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Diagnostics
10+
{
11+
/// <summary>
12+
/// Provides an extensiblity point for changing the behavior of the DeveloperExceptionPageMiddleware.
13+
/// </summary>
14+
public interface IDeveloperPageExceptionFilter
15+
{
16+
/// <summary>
17+
/// An exception handling method that is used to either format the exception or delegate to the next handler in the chain.
18+
/// </summary>
19+
/// <param name="errorContext">The error context.</param>
20+
/// <param name="next">The next filter in the pipeline.</param>
21+
/// <returns>A task the completes when the handler is done executing.</returns>
22+
Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next);
23+
}
24+
}

src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>ASP.NET Core diagnostics middleware abstractions and feature interface definitions.</Description>
@@ -9,4 +9,8 @@
99
<PackageTags>aspnetcore;diagnostics</PackageTags>
1010
</PropertyGroup>
1111

12+
<ItemGroup>
13+
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
14+
</ItemGroup>
15+
1216
</Project>

src/Middleware/Diagnostics/ref/Microsoft.AspNetCore.Diagnostics.netcoreapp3.0.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Diagnostics
5959
{
6060
public partial class DeveloperExceptionPageMiddleware
6161
{
62-
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) { }
62+
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) { }
6363
[System.Diagnostics.DebuggerStepThroughAttribute]
6464
public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; }
6565
}

src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class DeveloperExceptionPageMiddleware
3131
private readonly IFileProvider _fileProvider;
3232
private readonly DiagnosticSource _diagnosticSource;
3333
private readonly ExceptionDetailsProvider _exceptionDetailsProvider;
34+
private readonly Func<ErrorContext, Task> _exceptionHandler;
3435

3536
/// <summary>
3637
/// Initializes a new instance of the <see cref="DeveloperExceptionPageMiddleware"/> class
@@ -40,12 +41,14 @@ public class DeveloperExceptionPageMiddleware
4041
/// <param name="loggerFactory"></param>
4142
/// <param name="hostingEnvironment"></param>
4243
/// <param name="diagnosticSource"></param>
44+
/// <param name="filters"></param>
4345
public DeveloperExceptionPageMiddleware(
4446
RequestDelegate next,
4547
IOptions<DeveloperExceptionPageOptions> options,
4648
ILoggerFactory loggerFactory,
4749
IWebHostEnvironment hostingEnvironment,
48-
DiagnosticSource diagnosticSource)
50+
DiagnosticSource diagnosticSource,
51+
IEnumerable<IDeveloperPageExceptionFilter> filters)
4952
{
5053
if (next == null)
5154
{
@@ -57,12 +60,24 @@ public DeveloperExceptionPageMiddleware(
5760
throw new ArgumentNullException(nameof(options));
5861
}
5962

63+
if (filters == null)
64+
{
65+
throw new ArgumentNullException(nameof(filters));
66+
}
67+
6068
_next = next;
6169
_options = options.Value;
6270
_logger = loggerFactory.CreateLogger<DeveloperExceptionPageMiddleware>();
6371
_fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider;
6472
_diagnosticSource = diagnosticSource;
6573
_exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _options.SourceCodeLineCount);
74+
_exceptionHandler = DisplayException;
75+
76+
foreach (var filter in filters.Reverse())
77+
{
78+
var nextFilter = _exceptionHandler;
79+
_exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter);
80+
}
6681
}
6782

6883
/// <summary>
@@ -91,7 +106,7 @@ public async Task Invoke(HttpContext context)
91106
context.Response.Clear();
92107
context.Response.StatusCode = 500;
93108

94-
await DisplayException(context, ex);
109+
await _exceptionHandler(new ErrorContext(context, ex));
95110

96111
if (_diagnosticSource.IsEnabled("Microsoft.AspNetCore.Diagnostics.UnhandledException"))
97112
{
@@ -110,15 +125,15 @@ public async Task Invoke(HttpContext context)
110125
}
111126

112127
// Assumes the response headers have not been sent. If they have, still attempt to write to the body.
113-
private Task DisplayException(HttpContext context, Exception ex)
128+
private Task DisplayException(ErrorContext errorContext)
114129
{
115-
var compilationException = ex as ICompilationException;
130+
var compilationException = errorContext.Exception as ICompilationException;
116131
if (compilationException != null)
117132
{
118-
return DisplayCompilationException(context, compilationException);
133+
return DisplayCompilationException(errorContext.HttpContext, compilationException);
119134
}
120135

121-
return DisplayRuntimeException(context, ex);
136+
return DisplayRuntimeException(errorContext.HttpContext, errorContext.Exception);
122137
}
123138

124139
private Task DisplayCompilationException(
@@ -215,7 +230,7 @@ private Task DisplayRuntimeException(HttpContext context, Exception ex)
215230
}
216231
}
217232
}
218-
233+
219234
var request = context.Request;
220235

221236
var model = new ErrorPageModel

src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Linq;
76
using System.Diagnostics;
8-
using System.Threading;
97
using System.Threading.Tasks;
108
using Microsoft.AspNetCore.Builder;
119
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.Http;
1211
using Microsoft.AspNetCore.TestHost;
1312
using Microsoft.Extensions.DependencyInjection;
1413
using Xunit;
@@ -46,6 +45,88 @@ public async Task UnhandledErrorsWriteToDiagnosticWhenUsingExceptionPage()
4645
Assert.Null(listener.DiagnosticHandledException?.Exception);
4746
}
4847

48+
[Fact]
49+
public async Task ExceptionPageFiltersAreApplied()
50+
{
51+
// Arrange
52+
var builder = new WebHostBuilder()
53+
.ConfigureServices(services =>
54+
{
55+
services.AddSingleton<IDeveloperPageExceptionFilter, ExceptionMessageFilter>();
56+
})
57+
.Configure(app =>
58+
{
59+
app.UseDeveloperExceptionPage();
60+
app.Run(context =>
61+
{
62+
throw new Exception("Test exception");
63+
});
64+
});
65+
var server = new TestServer(builder);
66+
67+
// Act
68+
var response = await server.CreateClient().GetAsync("/path");
69+
70+
// Assert
71+
Assert.Equal("Test exception", await response.Content.ReadAsStringAsync());
72+
}
73+
74+
[Fact]
75+
public async Task ExceptionFilterCallingNextWorks()
76+
{
77+
// Arrange
78+
var builder = new WebHostBuilder()
79+
.ConfigureServices(services =>
80+
{
81+
services.AddSingleton<IDeveloperPageExceptionFilter, PassThroughExceptionFilter>();
82+
services.AddSingleton<IDeveloperPageExceptionFilter, AlwaysBadFormatExceptionFilter>();
83+
services.AddSingleton<IDeveloperPageExceptionFilter, ExceptionMessageFilter>();
84+
})
85+
.Configure(app =>
86+
{
87+
app.UseDeveloperExceptionPage();
88+
app.Run(context =>
89+
{
90+
throw new Exception("Test exception");
91+
});
92+
});
93+
var server = new TestServer(builder);
94+
95+
// Act
96+
var response = await server.CreateClient().GetAsync("/path");
97+
98+
// Assert
99+
Assert.Equal("Bad format exception!", await response.Content.ReadAsStringAsync());
100+
}
101+
102+
[Fact]
103+
public async Task ExceptionPageFiltersAreAppliedInOrder()
104+
{
105+
// Arrange
106+
var builder = new WebHostBuilder()
107+
.ConfigureServices(services =>
108+
{
109+
services.AddSingleton<IDeveloperPageExceptionFilter, AlwaysThrowSameMessageFilter>();
110+
services.AddSingleton<IDeveloperPageExceptionFilter, ExceptionMessageFilter>();
111+
services.AddSingleton<IDeveloperPageExceptionFilter, ExceptionToStringFilter>();
112+
})
113+
.Configure(app =>
114+
{
115+
app.UseDeveloperExceptionPage();
116+
app.Run(context =>
117+
{
118+
throw new Exception("Test exception");
119+
});
120+
});
121+
var server = new TestServer(builder);
122+
123+
// Act
124+
var response = await server.CreateClient().GetAsync("/path");
125+
126+
// Assert
127+
Assert.Equal("An error occurred", await response.Content.ReadAsStringAsync());
128+
}
129+
49130
public static TheoryData CompilationExceptionData
50131
{
51132
get
@@ -140,5 +221,45 @@ public CustomCompilationException(IEnumerable<CompilationFailure> compilationFai
140221

141222
public IEnumerable<CompilationFailure> CompilationFailures { get; }
142223
}
224+
225+
public class ExceptionMessageFilter : IDeveloperPageExceptionFilter
226+
{
227+
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
228+
{
229+
return context.HttpContext.Response.WriteAsync(context.Exception.Message);
230+
}
231+
}
232+
233+
public class ExceptionToStringFilter : IDeveloperPageExceptionFilter
234+
{
235+
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
236+
{
237+
return context.HttpContext.Response.WriteAsync(context.Exception.ToString());
238+
}
239+
}
240+
241+
public class AlwaysThrowSameMessageFilter : IDeveloperPageExceptionFilter
242+
{
243+
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
244+
{
245+
return context.HttpContext.Response.WriteAsync("An error occurred");
246+
}
247+
}
248+
249+
public class AlwaysBadFormatExceptionFilter : IDeveloperPageExceptionFilter
250+
{
251+
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
252+
{
253+
return next(new ErrorContext(context.HttpContext, new FormatException("Bad format exception!")));
254+
}
255+
}
256+
257+
public class PassThroughExceptionFilter : IDeveloperPageExceptionFilter
258+
{
259+
public Task HandleExceptionAsync(ErrorContext context, Func<ErrorContext, Task> next)
260+
{
261+
return next(context);
262+
}
263+
}
143264
}
144265
}

0 commit comments

Comments
 (0)