When comparing API's, I see a lot of different ways how error messages are returned. With the introduction of Problem Details for HTTP APIs (https://tools.ietf.org/html/rfc7807) , we finally have a standardized error payload to return when an unhandled exception occurs.
Although the standard was introduced before .NET 7, there was no out-of-the-box way to introduce the ProblemDetails spec into your ASP.NET Core application.
A solution was to use the third party Hellang.Middleware.ProblemDetails nuget package:
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel.DataAnnotations; | |
using System.Net.Http; | |
using System.Threading.Tasks; | |
using Hellang.Middleware.ProblemDetails.Mvc; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
namespace Example | |
{ | |
public class Startup | |
{ | |
public Startup(IWebHostEnvironment environment) | |
{ | |
Environment = environment; | |
} | |
private IWebHostEnvironment Environment { get; } | |
public static void Main(string[] args) | |
{ | |
CreateHostBuilder(args).Build().Run(); | |
} | |
public static IHostBuilder CreateHostBuilder(string[] args) | |
{ | |
return Host.CreateDefaultBuilder(args) | |
.UseEnvironment(Environments.Development) | |
//.UseEnvironment(Environments.Production) // Uncomment to remove exception details from responses. | |
.ConfigureWebHostDefaults(web => | |
{ | |
web.UseStartup<Startup>(); | |
}); | |
} | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddProblemDetails(ConfigureProblemDetails) | |
.AddControllers() | |
// Adds MVC conventions to work better with the ProblemDetails middleware. | |
.AddProblemDetailsConventions() | |
.AddJsonOptions(x => x.JsonSerializerOptions.IgnoreNullValues = true); | |
} | |
public void Configure(IApplicationBuilder app) | |
{ | |
app.UseProblemDetails(); | |
app.UseRouting(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapControllers(); | |
}); | |
} | |
private void ConfigureProblemDetails(ProblemDetailsOptions options) | |
{ | |
// Only include exception details in a development environment. There's really no need | |
// to set this as it's the default behavior. It's just included here for completeness :) | |
options.IncludeExceptionDetails = (ctx, ex) => Environment.IsDevelopment(); | |
// You can configure the middleware to re-throw certain types of exceptions, all exceptions or based on a predicate. | |
// This is useful if you have upstream middleware that needs to do additional handling of exceptions. | |
options.Rethrow<NotSupportedException>(); | |
// This will map NotImplementedException to the 501 Not Implemented status code. | |
options.MapToStatusCode<NotImplementedException>(StatusCodes.Status501NotImplemented); | |
// This will map HttpRequestException to the 503 Service Unavailable status code. | |
options.MapToStatusCode<HttpRequestException>(StatusCodes.Status503ServiceUnavailable); | |
// Because exceptions are handled polymorphically, this will act as a "catch all" mapping, which is why it's added last. | |
// If an exception other than NotImplementedException and HttpRequestException is thrown, this will handle it. | |
options.MapToStatusCode<Exception>(StatusCodes.Status500InternalServerError); | |
} | |
} | |
} |
Starting from .NET 7 this nuget package is no longer necessary. You only need to add the following line to your service configuration:
var builder = WebApplication.CreateBuilder(args); | |
builder.Services.AddControllers(); | |
builder.Services.AddProblemDetails(); |
If someone now calls your API and an exception occurs, the returned result will look like this:
{ | |
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", | |
"title": "Bad Request", | |
"status": 400 | |
} |
We can further customize the behavior through CustomizeProblemDetails:
builder.Services.AddProblemDetails(options => | |
options.CustomizeProblemDetails = ctx => | |
{ | |
ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName)); | |
// Add other custom problem details | |
} | |
); |