Problem Detail provide machine-readable details of errors in a HTTP response to avoid the need to define new error response formats for HTTP APIs based on https://tools.ietf.org/html/rfc7807. In the older version of .net doesn't produce a standardized error payload when an unhandled exception occurs and to handle this approach, documentation mention to use third-party library with name of Hellang Middleware ProblemDetails. It's a great library to map our exception to problem details payload.
In .Net 7 eventually support this feature using the IProblemDetailsService, and we can map our exception to problem details format. In the next we point some aspect of this new feature in .net 7.
Config problem details for all HTTP client and server error
We need following code to register problem details to service collection for generate standard problem details response:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails(); // Register problem details
var app = builder.Build();
app.MapControllers();
app.Run();
Let's jump to create an example for test problem details with an unhandled exception:
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
//api/values/SimpleTest/0
[HttpGet("{number}")]
public IActionResult SimpleTest(int number)
{
if (number <= 0)
{
throw new Exception("The number is less than or equal to 0!");
}
return Ok(number);
}
If we have some exception base on previous example, we will have a problem details response like below:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title": "System.Exception",
"status": 500,
"detail": "The number is less than or equal to 0!",
"traceId": "00-1bb2c6bbfe99c8d6db6a49d7116259fc-1be4dd33a66ac5f3-00",
"exception": { ... }
}
Custom problem details with ProblemDetailsOptions
We can config problem details with ProblemDetailsOptions
like below:
Before config our custom problem details, we need to create following code that we need them in our example:
public class ErrorFeature
{
public ErrorType Error { get; set; }
}
public enum ErrorType
{
ArgumentException
}
Note: If
ErrorFeature
in theProblemDetailsOptions
had value, will be set these maps for custom errors.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx => // Add custom problem details
{
var errorFeature = ctx.HttpContext.Features.Get<ErrorFeature>();
if (errorFeature is not null)
{
(string Title, string Detail) details = errorFeature.Error switch
{
ErrorType.ArgumentException =>
(
nameof(ArgumentException),
"We got argument-exception!"
),
_ =>
(
nameof(Exception),
"We got default-exception!"
)
};
ctx.ProblemDetails.Title = details.Title;
ctx.ProblemDetails.Detail = details.Detail;
}
}
);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.Run();
Let's check previous config with an example:
// api/values/CustomErrorTest/0
[HttpGet("{number}")]
public IActionResult CustomErrorTest(int number)
{
if (number <= 0)
{
var errorType = new ErrorFeature
{
Error = ErrorType.ArgumentException
};
HttpContext.Features.Set(errorType);
return BadRequest();
}
return Ok(number);
}
If we have some exception base on previous example, we will have a response that handle with ProblemDetailsOptions
like below:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "ArgumentException",
"status": 400,
"detail": "We got argument-exception!",
"traceId": "00-dc886373dd7c1895785d110c301d9432-25e55729e418542e-00"
}
Note: We can also add additional parameter to problem details with
ProblemDetailsOptions
like below:
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));
The result will be like this:
{
"type": "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1",
"title": "ArgumentException",
"status": 400,
"detail": "We got argument-exception!",
"traceId": "00-48fc51207f6996ba965ba1951c654ffa-d4d216d3756473fd-00",
"nodeId": "machine-name"
}
Problem details middleware for map exceptions
We can use problem details Middleware
instead of using ProblemDetailsOptions
and map our exceptions easily.
The following code display how we can config problem details with middleware:
var builder = WebApplication.CreateBuilder(args);
var env = builder.Environment;
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseStatusCodePages();
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.ContentType = "application/problem+json";
if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
{
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
var exceptionType = exceptionHandlerFeature?.Error;
if (exceptionType is not null)
{
(string Title, string Detail, int StatusCode) details = exceptionType switch
{
CustomException customException =>
(
exceptionType.GetType().Name,
exceptionType.Message,
context.Response.StatusCode = (int)customException.StatusCode
),
_ =>
(
exceptionType.GetType().Name,
exceptionType.Message,
context.Response.StatusCode = StatusCodes.Status500InternalServerError
)
};
var problem = new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = details.Title,
Detail = details.Detail,
Status = details.StatusCode
}
};
if (env.IsDevelopment())
{
problem.ProblemDetails.Extensions.Add("exception", exceptionHandlerFeature?.Error.ToString());
}
await problemDetailsService.WriteAsync(problem);
}
}
});
});
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.Run();
Let's run an example for check previous configuration for handle exception with custom problem details middleware:
Before run the example we need create a custom exception for use it in our test sample like the following:
public class CustomException : Exception
{
public CustomException(
string message,
HttpStatusCode statusCode = HttpStatusCode.BadRequest
) : base(message)
{
StatusCode = statusCode;
}
public HttpStatusCode StatusCode { get; }
}
// api/values/CustomExceptionTest/0
[HttpGet("{number}")]
public IActionResult CustomExceptionTest(int number)
{
if (number <= 0)
{
throw new CustomException("Some custom exception!");
}
return Ok(number);
}
If we have some exception base on previous example, we will have a custom problem details response like below:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "CustomException",
"status": 400,
"detail": "Some custom exception!",
"exception": "Exception with more details..."
}
Note: For handling
HttpStatusCode
(404
,401
,405
and etc) error with format problem details. We can add this middleware:
app.UseStatusCodePages();
You can find the sample code in this repository:
🔗 https://github.com/meysamhadeli/blog-samples/tree/main/src/problem-details-sample
Conclusion
In this post, I talk about new feature Problem Details
that introduce in .Net 7 to handle exceptions with different way. Also, we can also use alternative way with using great library Hellang Middleware ProblemDetails.
Reference
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-7.0#pds7 https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-7.0#pds7