As many of you know, I am primarily a backend developer, and as such, I create a lot of APIs and participate in both their design and implementation. Something I've always fought for is the unification of API responses, which I wrote a post about a few years ago. Some time has passed since that post, and I've noticed that the companies I work with are starting to implement the standard, so let's talk about it here.
As always, you can find the code available at the GitHub link.
Table of Contents
1 - ProblemDetails Standard for HTTP APIs
The idea behind the RFC-9457 standard, published in July 2023, is to finally normalize how we create web APIs, specifically when handling errors.
When everything goes well, there is no problem; we return an object containing the information, and this has been working this way for years.
The problem comes when we have an error. What happens if validation fails? Or what if we have an unexpected error, how do we return that information?
This is where Problem Details comes into play, and the standard is necessary because every company did whatever they wanted, and that is a mess.
Some companies return just the HTTP Status code, others return the Status Code with a plain text string, which can be read by humans but NOT by a machine, well, it can, but you'd need a regular expression. Others, as used to be my case, had a custom object indicating the responses for the API in their company.
So it's time to normalize and standardize everything.
Note: Previously, we had the RFC-7807 standard, but personally, I never worked at a company that implemented it.
2 - What is Problem Details?
At the end of the day, the standard, or Problem Details, is nothing more than a standardized JSON that contains the fields Type, title, status, details, instance, and then extension members.
The members themselves are all optional, but obviously, the more we include, the better. If we're building public or semi-public APIs, it's normal to fill them all. When it's B2B communication, in my experience, it's most common to use just the type with the title and maybe the description; plus, the API will respond with the type "application/problem.json".
2.1 - Members of Problem Details
For this scenario, let's imagine we have an API that allows us to create items in different accounts, like a store where you are the supervisor of several.
A - Type
The type member is a string that ideally contains a URL pointing to the error explanation in the documentation. For example:
https://api.miweb.com/referencia/WRONG_SHOPID
In B2B communication, it's very common to return only the code itself rather than the URL, as the documentation is not always public.
B - Title
This field is just for a human, so that when they read it, they can understand the problem. For example:
“Tienda no encontrada”
C - Detail
The detail field is an extension of the previous one, and its focus is to provide a more specific error. For example:
“Has intentado crear un producto para la tienda 1 pero solo tienes acceso a la tienda 2 y 3”
D - Instance
Represents a unique identifier for tracking the problem; many times it's a UUID, other times a numeric ID. It's used to follow up on the error. In many companies, it's the correlationId.
The returned value is usually just the ID or a full URL pointing to where you can track it, for example:
https://api.miweb.com/errorcase/11111
E - Status
Represents the HTTP status code, normally, it will be an error code, but I've also seen companies return 200s even on errors... In my opinion, that's incorrect, but that's a story for another post.
F - Extensions
Basically, an object that contains additional details about the error. It is handled like a key-value dictionary inside, the keys will be unique, but you can include as many as you want.
If we look at the example from the standard's website:
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345",
"/account/67890"]
}
You can see they have the properties balance and accounts; this means we are not limited by the standard, but can extend it as much as we want.
3 - Problem Details in .NET and C#
Now it's time to mention the implementation we have in .NET, because by default, ASP.NET comes with this system, if you have an endpoint expecting an integer but send a string, the error returned by ASP.NET is a ProblemDetails.
For example, in our code, in the education endpoint:
[HttpGet("{userId}")]
public Task<List<EducationDto>> Get(int userId)
{
...
}
If we send a string, we will get the following error:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"userId": [
"The value 'test' is not valid."
]
},
"traceId": "00-3a4cb46525de8b9f4da6c3fe9be36e3f-9d5836f4fd45f23c-00"
}
As we can see, ASP.NET is returning a ProblemDetails object that contains type, title, and status. It also includes two custom ASP.NET properties: errors, which indicate there were errors, and traceId, which in my opinion should be "instance".
3.1 - Implementing ProblemDetails in C#
What we're going to do now is quite simple, we're going to implement this object's behavior. Right now in our use case for creating a profile, we don't have any validation, e.g., we aren't validating if the email or phone number are valid; this call works perfectly:
POST {{endpoint}}/api/PerfilPersonal
Content-Type: application/json
{
"UserId": null,
"Id": null,
"UserName": "test-1",
"FirstName": "firstName",
"LastName": "last name",
"Description": "Description",
"Phone": "telefono",
"Email": "email",
"Website": "web",
"GitHub": "github",
"Interests": [],
"Skills": []
}
But obviously, this is not what we want. We need it to return an error. For now, we can add this logic in the controller.
- NOTE: This logic should be in the business logic, but this is just a learning app.
So we create the ProblemDetails
object directly inside the post method:
[HttpPost]
public async Task<IActionResult> Post(PersonalProfileDto profileDto)
{
👇
ProblemDetails problemDetails = new ProblemDetails()
{
Title = "Error en la validación de los datos",
Detail = "Los parámetros no son correctos",
Status = 400,
Type = "https://website.net/code-error-1",
};
return await _postPersonalProfile.Create(profileDto)
.Bind(x => GetProfile(x.UserName))
.ToActionResult();
}
For now, it doesn't do anything. What we're going to do is: if the email is not valid, include an error, and the same for the phone, if it isn't numeric, give an error;
Then it will return the ProblemDetails object, and as you can see, we've included an extensible parameter called "Errors" that contains both errors:
[HttpPost]
public async Task<IActionResult> Post(PersonalProfileDto profileDto)
{
ProblemDetails problemDetails = new ProblemDetails()
{
Title = "Error en la validación de los datos",
Detail = "Los parámetros no son correctos",
Status = 400,
Type = "https://website.net/code-error-1",
};
👇
List<(string, string)> errors = new();
//Validar email
if (!IsValidEmail(profileDto.Email))
{
errors.Add(("Email", "El email no es válido"));
}
//validar numero de telefono es un numero
if (!long.TryParse(profileDto.Phone, out _))
{
errors.Add(("Phone", "El número de teléfono no es válido"));
}
if (errors.Any())
{
problemDetails.Extensions.Add("Errors", errors.ToDictionary());
return new ObjectResult(problemDetails);
}
👆
return await _postPersonalProfile.Create(profileDto)
.Bind(x => GetProfile(x.UserName))
.ToActionResult();
}
Now, if we run the code, we see that it fails, just as expected.
{
"type": "https://website.net/code-error-1",
"title": "Error en la validación de los datos",
"status": 400,
"detail": "Los parámetros no son correctos",
"Errors": {
"Email": "El email no es válido",
"Phone": "El número de teléfono no es válido"
}
}
Before wrapping up, I want to repeat that this logic should be in the business logic and not in the controller. In this case, it's not because I use the ROP library I created a while ago, and for now, it doesn't support converting to ProblemDetails, but the idea is to create an extension so this is possible and happens automatically. You can contribute the change yourself here.
If there is any problem you can add a comment bellow or contact me in the website's contact form