-
Notifications
You must be signed in to change notification settings - Fork 704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom error handling in .NET6 #1019
Comments
In .NET 6 and, hence, What you are looking for is the IProblemDetailsFactory service. The change and customization will depend on whether you are using Minimal APIs or MVC Core. The DefaultProblemDetailsFactory is used for Minimal APIs, whereas MvcProblemDetailsFactory provides a decorated adapter over
|
This question has been asked before, but I guess it's not in the documentation. There have never been any specific examples for changing the behavior. I've updated the Error Responses topic in the wiki to highlight the differences. |
Actually the reason that I ask is, I am looking for a way to customize the error response format to continue following the Microsoft REST Guidelines error response format, as we have already released our v1 API using this format.
We are using OData so we are unable to use the official Microsoft versioning package. We tried to switch to this package, but we lost the ability to register our implementation of We were hoping to not have to use Problem Details as that would break our API compatibility for existing customers in our v1. Is it possible to intercept/override something to continue to format the errors to match the Microsoft REST Guidelines. |
I'm not sure I follow:
There is no other package or project for ASP.NET API Versioning. This is the one and only. You can thank Microsoft for the name change. Now that I know what you are trying to achieve, let me see if I can't provide an example of how to achieve the results you're looking for. 😄 |
It was surprisingly easier than I thought it would be. Here is a rough solution that would work: using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using System.Text.Json.Serialization;
public class ErrorObjectConverter : JsonConverter<ProblemDetails>
{
public override void Write(
Utf8JsonWriter writer,
ProblemDetails value,
JsonSerializerOptions options )
{
if ( IsSupported( value ) )
{
WriteErrorObject( writer, value, options );
}
else
{
options = new JsonSerializerOptions( options );
options.Converters.Remove( this );
JsonSerializer.Serialize( writer, value, options );
}
}
protected virtual void WriteErrorObject(
Utf8JsonWriter writer,
ProblemDetails problemDetails,
JsonSerializerOptions options )
{
writer.WriteStartObject();
writer.WriteStartObject( "error" );
if ( problemDetails.Extensions.TryGetValue( "code", out var value ) )
{
writer.WriteString( "code", value.ToString() );
}
writer.WriteString( "message", problemDetails.Title );
if ( !string.IsNullOrEmpty( problemDetails.Instance ) )
{
writer.WriteString( "target", problemDetails.Instance );
}
if ( !string.IsNullOrEmpty( problemDetails.Detail ) )
{
writer.WriteStartArray( "details" );
writer.WriteStringValue( problemDetails.Detail );
writer.WriteEndArray();
}
writer.WriteEndObject();
writer.WriteEndObject();
}
public override ProblemDetails Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options ) => throw new NotSupportedException();
private static bool IsSupported( ProblemDetails problemDetails )
{
if ( problemDetails.Type == ProblemDetailsDefaults.Unsupported.Type )
{
return true;
}
if ( problemDetails.Type == ProblemDetailsDefaults.Unspecified.Type )
{
return true;
}
if ( problemDetails.Type == ProblemDetailsDefaults.Invalid.Type )
{
return true;
}
if ( problemDetails.Type == ProblemDetailsDefaults.Ambiguous.Type )
{
return true;
}
return false;
}
} There are a few ways you can register the converter, but the simplest method would look like: // CAUTION: note the ambiguity between:
// - Microsoft.AspNetCore.Http.Json.JsonOptions
// - Microsoft.AspNetCore.Mvc.JsonOptions
//
// You need the former as that is purely routing without any part of MVC
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(
options => options.SerializerOptions.Converters.Insert( 0, new ErrorObjectConverter() ) );
// this should not be necessary, but note that you can cover MVC Core paths with the same converter
// builder.Services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(
// options => options.JsonSerializerOptions.Converters.Insert( 0, new ErrorObjectConverter() ) ); Let's say a client requests {
"type": "https://docs.api-versioning.org/problems#unsupported",
"title": "Unsupported API version",
"status": 400,
"detail": "The HTTP resource that matches the request URI 'https://localhost:5001/api/values' does not support the API version '42.0'.",
"code": "UnsupportedApiVersion",
"traceId": "00-9956265f482c96f8478349bb1403427e-f7267eaaa2a7891d-00"
} However, with the converter in place, it will now produce: {
"error":
{
"code": "UnsupportedApiVersion",
"message": "Unsupported API version",
"details":
[
"The HTTP resource that matches the request URI 'https://localhost:5001/api/values' does not support the API version '42.0'."
]
}
} The example provided will only convert I haven't received many requests for it, but this should be a generic solution that can provide a solid backward compatibility story, which - to be honest - is missing. I need a little bit more time to think about the edge cases, configuration, and testing, but this approach should unblock you in the meantime. Any other thoughts or feedback you have are welcome. |
I have tried enabling ProblemDetails in .NET6 by adding Hellang.Middleware.ProblemDetails nuget package. By doing this, when I specify an unsupported/invalid API version, I simply get a response: How are you enabling ProblemDetails for API versioning in .NET 6? |
Danger Will Robinson! <you are going down the proverbial rabbit hole 😉>
This means it was possible to generate aspnet-api-versioning/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs Line 35 in c462195
Unfortunately, this doesn't cover every single edge case because ASP.NET Core itself doesn't provide Problem Details or hooks to provide one for some cases such as
If the routing system lands on any one of these endpoints, you'll get a Line 79 in c462195
The idea is that you may have already changed MVC Core's default implementation of Using the |
I believe to have found the confusion of why I thought ProblemDetails were not enabled. In API Versioning 5.x, we were able to set In API Versioning 6.x, we no longer have that ability and you are proposing we implement a However, when versioning by URL segment only, we get a 404 response, with no body (ie. no ProblemDetails). When we enable the other versioning reader methods (query string and/or http header), we do get ProblemDetails when providing the version by 1 of these methods. We are only using URL segment versioning, and in API Versioning 5.x we were able to rewrite the errors using the Additionally, the HTTP Status Code has changed between 5.x and 6.x for URL segment only, from 400 to 404. This is also a breaking change. |
We would continue to use 5.x if we could, but we need the OData changes in 6.x. This is coming to light since we are now moving onto version 2 of our API; this was not a problem when we only had the 1 version. |
Much of this was called out in discussion #808; specifically the breaking changes. You might have missed it or perhaps it was TL;DR, which I can understand. The routing behavior is a bit tricky. Due some other bugs (specifically versioning by media type), I had to push things down further into the routing system. Aside from being not RESTful, versioning by URL segment has been the bane of the codebase. There are lot of strange and painful edge cases to solve that only apply to that approach. The issue here is that route constraints have not been yet evaluated. Processing it out of the route constraint is painful enough, but the real issue is at this stage in the routing system, there's no clear way to know if there might be a match versus not. This is why you now get
If you request It's not lost on me that, however, creates a behavioral breaking change. Another feature that was lost was reporting API versions, when the request is unmatched. If you don't match an endpoint, how can you possibly report supported or deprecated API versions? The community gave me a challenge and I was eventually able to come up with a best effort report scheme, which to my knowledge always (or almost always) reports the expected API versions due to how the routing system groups endpoints. In the case of Another approach would be to keep your V1 on the You may not have to implement a 5.x does not support .NET 6. It specifically targets .NET 5.0. You should be very careful about using versions that do not align to the .NET or ASP.NET Core runtime. While it might work, know that it may not. I've seen new features cause runtime failures. The 5.x and Microsoft.* packages are essentially EOL and will not have any more updates save for critical servicing updates. The powers that be decided to not retain co-ownership so it's impossible to for me to continue evolving the project under the Microsoft branding. It was quite painful just getting this far. I considered forking the repo, but I think that would have just been even more confusing to the community. I will likely upload a patch package that indicates it is deprecated, which I think I can do - now. Hopefully, that will bring more additional the banner and other announcements that have been made over the last 18+ months. There are a lot of people out there that thought or still think this project is owned and operated by the ASP.NET team. It isn't. It never was. I've run the project virtually solo the whole time. When I left Microsoft, it caused a lot of churn for the community unfortunately. I fought for nearly a year to do the right thing, but alas my hands are tied. I continue to be committed to helping the community navigate these changes. I'm not sure that I can fix the |
It's unfortunate that there is a breaking change in design between ASP.NET Core 6.0 Setupbuilder.Services.AddSingleton<IProblemDetailsFactory, ErrorObjectFactory>();
builder.Services.AddApiVersioning().AddMvc(); Starting in .NET 7.0 and beyond, it will be an ever-so-slightly different setup. Unfortunately, registration order still matters, but this is more about how the ASP.NET Core 7.0+ Setupbuilder.Services.AddControllers();
builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton<IProblemDetailsWriter, ErrorObjectWriter>() );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning().AddMvc(); I'll let this settle for a few days so that you have a chance to look things over and provide feedback - if you want to. I don't have a good out-of-the-box solution for the |
|
I believe this issue has reached its logical conclusion. The older error responses are now supported with a path to customize it further. This is a great addition to helping people migrate and will be added to the migration guide. Thanks for pushing it. If I've missed something or additional assistance is required, don't hesitate to reply or reopen the issue. |
Is there an existing issue for this?
Describe the bug
In the releases of v6.x.x of this package, IErrorResponseProvider was removed in favor of ProblemDetails.
All the examples show how to
.AddProblemDetails()
but unfortunately, they all target .NET7.How is this supposed to be handled in .NET6? I need to be able to support backwards compatibility for my error messages, but this change does not really explain how to achieve this?
Expected Behavior
No response
Steps To Reproduce
No response
Exceptions (if any)
No response
.NET Version
6
Anything else?
No response
The text was updated successfully, but these errors were encountered: