Skip to content

Commit 137d04d

Browse files
authored
Add an option on the ExceptionHandlerOptions to select status codes based on exceptions (dotnet#56616)
1 parent 20c68dc commit 137d04d

File tree

7 files changed

+132
-8
lines changed

7 files changed

+132
-8
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/FunctionalTests/ProblemDetailsExceptionHandlerSampleTest.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.AspNetCore.Mvc;
77
using System.Net.Http.Json;
88
using System.Net.Http.Headers;
9+
using Microsoft.AspNetCore.Http;
910

1011
namespace Microsoft.AspNetCore.Diagnostics.FunctionalTests;
1112

@@ -34,4 +35,21 @@ public async Task ExceptionHandlerPage_ProducesProblemDetails()
3435
Assert.NotNull(body);
3536
Assert.Equal(500, body.Status);
3637
}
38+
39+
[Fact]
40+
public async Task StatusCodeSelector_ProducesProblemDetailsWithCustomStatusCode()
41+
{
42+
// Arrange
43+
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/throw2/conflict");
44+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
45+
46+
// Act
47+
var response = await Client.SendAsync(request);
48+
49+
// Assert
50+
var body = await response.Content.ReadFromJsonAsync<ProblemDetails>();
51+
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
52+
Assert.NotNull(body);
53+
Assert.Equal(StatusCodes.Status409Conflict, body.Status);
54+
}
3755
}

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 StatusCodeSelector_CanSelect404()
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
{
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace ExceptionHandlerSample;
5+
6+
public class ConflictException(string message) : Exception(message);

src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/StartupWithProblemDetails.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,28 @@ public void ConfigureServices(IServiceCollection services)
1616

1717
public void Configure(IApplicationBuilder app)
1818
{
19-
// Configure the error handler to produces a ProblemDetails.
20-
app.UseExceptionHandler();
21-
2219
// The broken section of our application.
2320
app.Map("/throw", throwApp =>
2421
{
25-
throwApp.Run(context => { throw new Exception("Application Exception"); });
22+
// Configure the error handler to produce ProblemDetails.
23+
throwApp.UseExceptionHandler();
24+
throwApp.Run(_ => throw new Exception("Application Exception"));
25+
});
26+
27+
app.Map("/throw2", throwApp =>
28+
{
29+
// Configure the error handler to produce ProblemDetails with a mapped status code
30+
throwApp.UseExceptionHandler(new ExceptionHandlerOptions
31+
{
32+
StatusCodeSelector = ex => ex is ConflictException
33+
? StatusCodes.Status409Conflict
34+
: StatusCodes.Status500InternalServerError,
35+
});
36+
37+
throwApp.Map("/conflict", throwConflictApp =>
38+
{
39+
throwConflictApp.Run(_ => throw new ConflictException("Conflict Exception"));
40+
});
2641
});
2742

2843
app.UseStaticFiles();
@@ -32,7 +47,8 @@ public void Configure(IApplicationBuilder app)
3247
{
3348
context.Response.ContentType = "text/html";
3449
await context.Response.WriteAsync("<html><body>Welcome to the sample<br><br>\r\n");
35-
await context.Response.WriteAsync("Click here to throw an exception: <a href=\"/throw\">throw</a>\r\n");
50+
await context.Response.WriteAsync("Click here to throw an exception: <a href=\"/throw\">throw</a><br>\r\n");
51+
await context.Response.WriteAsync("Click here to throw a conflict exception: <a href=\"/throw2/conflict\">throw conflict</a>\r\n");
3652
await context.Response.WriteAsync("</body></html>\r\n");
3753
});
3854
}

0 commit comments

Comments
 (0)