Skip to content

Commit d525944

Browse files
committed
Add an option to select status codes based on exceptions on the ExceptionHandlerOptions
1 parent d301328 commit d525944

File tree

4 files changed

+87
-3
lines changed

4 files changed

+87
-3
lines changed

src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
165165

166166
context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature);
167167
context.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature);
168-
context.Response.StatusCode = DefaultStatusCode;
168+
context.Response.StatusCode = _options.StatusCodeSelector?.Invoke(edi.SourceException) ?? DefaultStatusCode;
169169
context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);
170170

171171
string? handler = null;
@@ -192,7 +192,7 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
192192
{
193193
HttpContext = context,
194194
AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata,
195-
ProblemDetails = { Status = DefaultStatusCode },
195+
ProblemDetails = { Status = context.Response.StatusCode },
196196
Exception = edi.SourceException,
197197
});
198198
if (handled)
@@ -202,7 +202,7 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
202202
}
203203
}
204204
// If the response has already started, assume exception handler was successful.
205-
if (context.Response.HasStarted || handled || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
205+
if (context.Response.HasStarted || handled || _options.StatusCodeSelector != null || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
206206
{
207207
const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException";
208208
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName))

src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,12 @@ public class ExceptionHandlerOptions
3838
/// the original exception.
3939
/// </summary>
4040
public bool AllowStatusCode404Response { get; set; }
41+
42+
/// <summary>
43+
/// Gets or sets a delegate used to map an exception to a http status code.
44+
/// </summary>
45+
/// <remarks>
46+
/// If <see cref="StatusCodeSelector"/> is <c>null</c>, the default exception status code 500 is used.
47+
/// </remarks>
48+
public Func<Exception, int>? StatusCodeSelector { get; set; }
4149
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.StatusCodeSelector.get -> System.Func<System.Exception!, int>?
3+
Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.StatusCodeSelector.set -> void

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,80 @@ public async Task ExceptionHandler_CanReturn404Responses_WhenAllowed()
656656
&& w.Message == "No exception handler was found, rethrowing original exception.");
657657
}
658658

659+
[Fact]
660+
public async Task ExceptionHandler_SelectsStatusCode()
661+
{
662+
using var host = new HostBuilder()
663+
.ConfigureWebHost(webHostBuilder =>
664+
{
665+
webHostBuilder
666+
.UseTestServer()
667+
.ConfigureServices(services => services.AddProblemDetails())
668+
.Configure(app =>
669+
{
670+
app.UseExceptionHandler(new ExceptionHandlerOptions
671+
{
672+
StatusCodeSelector = ex => ex is ApplicationException
673+
? StatusCodes.Status409Conflict
674+
: StatusCodes.Status500InternalServerError,
675+
});
676+
677+
app.Map("/throw", innerAppBuilder =>
678+
{
679+
innerAppBuilder.Run(_ => throw new ApplicationException("Something bad happened."));
680+
});
681+
});
682+
}).Build();
683+
684+
await host.StartAsync();
685+
686+
using (var server = host.GetTestServer())
687+
{
688+
var client = server.CreateClient();
689+
var response = await client.GetAsync("throw");
690+
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
691+
}
692+
}
693+
694+
[Fact]
695+
public async Task ExceptionHandler_SelectsStatusCode404_When404ThrowedAndNotNotAllowed()
696+
{
697+
using var host = new HostBuilder()
698+
.ConfigureWebHost(webHostBuilder =>
699+
{
700+
webHostBuilder
701+
.UseTestServer()
702+
.ConfigureServices(services => services.AddProblemDetails())
703+
.Configure(app =>
704+
{
705+
app.UseExceptionHandler(new ExceptionHandlerOptions
706+
{
707+
// 404 is not allowed,
708+
// but as the exception is explicitly mapped to 404 by the StatusCodeSelector,
709+
// it should be set anyway.
710+
AllowStatusCode404Response = false,
711+
StatusCodeSelector = ex => ex is ApplicationException
712+
? StatusCodes.Status404NotFound
713+
: StatusCodes.Status500InternalServerError,
714+
});
715+
716+
app.Map("/throw", innerAppBuilder =>
717+
{
718+
innerAppBuilder.Run(_ => throw new ApplicationException("Something bad happened."));
719+
});
720+
});
721+
}).Build();
722+
723+
await host.StartAsync();
724+
725+
using (var server = host.GetTestServer())
726+
{
727+
var client = server.CreateClient();
728+
var response = await client.GetAsync("throw");
729+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
730+
}
731+
}
732+
659733
[Fact]
660734
public async Task ExceptionHandlerWithOwnBuilder()
661735
{

0 commit comments

Comments
 (0)