12

Handling HTTP API Errors with Problem Details

 2 years ago
source link: https://codeopinion.com/handling-http-api-errors-with-problem-details/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

If you’re developing an HTTP API, having a consistent way of returning errors can make handling errors on the client-side simpler. I’ve talked about Problem Details in another video, which is a standard for returning machine-readable details of errors in an HTTP Response. In this video, I’ll cover how to consume those HTTP API Errors from the client-side.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Problem Details

If you’re unfamiliar, Problem Details (https://tools.ietf.org/html/rfc7807) is a standard for providing errors from your HTTP API to consuming clients.

HTTP [RFC7230] status codes are sometimes not sufficient to convey enough information about an error to be helpful. While humans behind Web browsers can be informed about the nature of the problem with an HTML [W3C.REC-html5-20141028] response body, non-human consumers of so-called “HTTP APIs” are usually not. This specification defines simple JSON [RFC7159] and XML [W3C.REC-xml-20081126] document formats to suit this purpose. They are designed to be reused by HTTP APIs, which can identify distinct “problem types” specific to their needs. Thus, API clients can be informed of both the high-level error class (using the status code) and the finer-grained details of the problem (using one of these formats).

If you’re interested in how to produce Problem Details from your HTTP API, check out my other post: Problem Details for Better REST HTTP API Errors.

For the rest of this post, I’m going to use a simple ASP.NET Core route that is returning a problem details response that we’ll interact with.

using System.Collections.Generic; using Microsoft.AspNetCore.Mvc;

namespace WebApplication.Controllers { [ApiController] [Route("[controller]")] public class DemoController : ControllerBase { [HttpPost] public ActionResult Post() { var problemDetails = new ProblemDetails { Detail = "The request parameters failed to validate.", Instance = null, Status = 400, Title = "Validation Error", Type = "https://example.net/validation-error", };

problemDetails.Extensions.Add("invalidParams", new List<ValidationProblemDetailsParam>() { new("name", "Cannot be blank."), new("age", "Must be great or equals to 18.") });

return new ObjectResult(problemDetails) { StatusCode = 400 }; } }

public class ValidationProblemDetailsParam { public ValidationProblemDetailsParam(string name, string reason) { Name = name; Reason = reason; }

public string Name { get; set; } public string Reason { get; set; } } }

Here’s a sample of what a Problem Details response body looks like. It will also have a Content-Type header of application/problem+json;

Consumer

To illustrate handling a Problem Details response in C#, we can provide the HttpClient with a custom HttpMessageHandler. This will allow us to inspect the response and check if the Content-Type response is of application/problem+json, and if it is, throw a ProblemDetailsException with the relevant properties.

using System; using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks;

namespace ConsoleApp1 { public class ProblemDetailsHttpMessageHandler : DelegatingHandler { public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { }

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct) { var response = await base.SendAsync(request, ct);

var mediaType = response.Content.Headers.ContentType?.MediaType; if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) { var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(null, ct) ?? new ProblemDetails(); throw new ProblemDetailsException(problemDetails, response); }

return response; } } }

Now when using an HttpClient, we can pass it our ProblemDetailsHttpMessageHandler that will throw and we can handle that specific exception.

using System; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks;

namespace ConsoleApp1 { class Program { static async Task Main(string[] args) { var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); try { var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); } catch (ProblemDetailsException problemDetailsException) { Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); Console.WriteLine($"Status: {problemDetailsException.Details.Status}");

if (problemDetailsException.Details.Type == "https://example.net/validation-error") { Console.WriteLine($"Validation Errors:"); var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); foreach (var invalidParam in validationErrors.InvalidParams) { Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); } }

Console.ReadKey(); } } } }

When I run this sample console application, here’s the output from all the Console.Writeline.

Extensions

The real power for consuming Problem Details as HTTP API Errors is in the Type property and Extensions. The Type property in the Problem Details response is a URI that should provide developers at design time information about the error. This can include additional properties, called extensions, that may also exist in the response body. In my example above, “invalidParams” was an extension. My client code, albeit trivial, was looking for that specific type so that it could deserialize the response to a specific validation problem details type that I created, that was fully aware of that “invalidParams” extension.

If you leverage the Type property you can build very specific error types using extensions that your client can be aware of to provide a better experience.

Javascript & Typescript

Alex Zeitler created a nice package for parsing Problem Details that you can use within a Javascript / Typescript client. Check out his package http-problem-details-parser.

Here’s a sample using a mapper for the invalidParams extension.

const mappers: HttpProblemExtensionMapper[] = [ { type: 'https://example.net/validation-error', map: (object: any) => new ProblemDocumentExtension({ 'invalid-params': object['invalid-params'] }) } ] const problemDetails = fromJSON(status400JSON, mappers);

Source Code

Developer-level members of my CodeOpinion YouTube channel get access to the full source for any working demo application that I post on my blog or YouTube. Check out the membership for more info.

Related Links

Follow @CodeOpinion on Twitter

Leave this field empty if you're human:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK