RESTful error handling with Akka HTTP and the library “endpoints”
January 29, 2020 | Technical Blog
RESTful services represent both technical errors and business errors at the same level and this raises some challenges to backend engineers. What we call “technical” errors here are errors returned by the server in case of an unexpected problem on the server or in case of mismatch between the format of request that the server expects and what a client sends. What we call “business” errors are errors returned by the business logic invoked by the server. Both types of errors are modeled at the same level because both are modeled as HTTP responses using 4xx or 5xx status codes.
This situation is challenging for engineers implementing RESTful services because both types of errors are produced by distinct components but should have a uniform representation. Indeed, technical errors are typically produced by an HTTP library, based on a global setting, whereas business errors are produced by the business logic, on a per-endpoint basis. Their representation should be uniform so that clients don’t surprisingly get an HTML response entity when they expect a JSON response entity, for instance.
Even with modern HTTP libraries, serving uniform technical and business errors does not come for free. By default, Play or Akka HTTP produce HTML or plain text technical errors, whereas backend implementations typically produce JSON business errors.
The problem is exacerbated when it comes to maintaining the documentation of an HTTP service. Ideally, all possible response types for each endpoint should appear, including both technical and business error responses. Keeping such documentation consistent with the actually used technical error handler is challenging: both parts of your system are so far from each other that it is too easy to forget to update one according to the other.
To summarize, the problem statement of this article is the following:
The remainder of the article shows how to build such an HTTP service with the library endpoints.
The library endpoints lets you describe your HTTP endpoints once, and then derives the server part, the client part, and documentation. It ensures that servers, clients, and documentation are consistent because they are all based on the same source of truth.
For instance, consider a service that exposes a greet endpoint responding with a greeting message to incoming requests. The request uses the verb GET, the URL /greet, and requires a query parameter name. The response uses the status code OK and contains a text entity with a greeting message. Here is how we describe this endpoint:
The trait algebra.Endpoints that we extend provides methods for describing endpoints. It defines how these descriptions can be combined together to build more complex descriptions. For this reason, it is called an “algebra”.
Here is how we get a server implementation for the greet endpoint description:
As you can see, implementing the server part consists of providing the business logic of the endpoint. The library endpoints takes care of:
In our case, the business logic is a String => String function, which takes a name and returns a greeting message.
The resulting route value is an Akka HTTP Route.
Here is how we get OpenAPI documentation from the greet endpoint description:
The resulting api value can be serialized into the following JSON document:
This OpenAPI document shows that our service exposes an endpoint on the path /greet, with the verb GET, and takes a string query parameter name, as we defined in the endpoint description!
This document can be fed into tools like Swagger Editor, which lets users browse the documentation from an interactive user interface like so:
You can play with this code example online.
If you look carefully at the endpoint responses shown in the OpenAPI document, you will notice that it mentions two response types that were not part of the endpoint description. These responses are documented as “Client error” and “Server error”, they use a status code of 400 and 500, respectively, and carry a JSON entity containing a list of errors. Where do they come from?
These “technical” response types are actually handled by the library endpoints itself. For instance, if an incoming request contains no name parameter, the library returns a 400 (Bad Request) response with an appropriate error message. Similarly, if an exception is thrown during the invocation of the business logic, the library returns a 500 (Internal Server Error) response with the relevant error message.
Now, consider the situation where our business logic may also return a Bad Request response. For instance, we could say that “Voldemort” is an invalid name and that we don’t want to reply “Hello, Voldemort!”. Instead, we would like to reply with a Bad Request response containing a message indicating that “Voldemort” is not an accepted name.
But remember that our generated OpenAPI documentation states that Bad Request responses return a JSON entity, so if we want our business logic to return such a response, it has to conform to what is stated in the documentation! We could also consider changing what is stated in the documentation, but, in that case, we need to make sure that technical errors handled by the library conform to the new documentation.
In fact, the library already enforces the use of the same type of entities for both technical and business errors. Here is how we can update our endpoint description to indicate that the business logic might also return a Bad Request response:
We have changed the response to badRequest().orElse(ok(textResponse)). Note that in case of success we indicate that the entity carried by the OK response is a text entity (via the textResponse parameter). However, we don’t indicate what type of entity is carried by Bad Request responses. That’s because we have no choice: for business errors, the library forces us to use the same type of entity as the one used for technical errors.
For the sake of completeness, here is how the business logic is implemented:
Business errors are modeled with values like Left(Invalid(“some error message”)), which are encoded into HTTP responses by the library.
The implementation of the documentation would not change. Neither would the produced OpenAPI document (Bad Request responses were already documented).
So far, we have seen that:
This situation is only half satisfactory, though. What if we want to use a different type of entity than the one internally used by the library for error responses? For instance, would it be possible to use Problem Details responses instead?
By default, the error entities returned by endpoints look like the following:
Instead, we want to return Problem Details entities like so:
To use a custom type of error entities, we have to indicate to the library endpoints that we don’t want to use the built-in types of error entities but provide our own error entities instead.
We achieve this by defining an alternative algebra to use in place of the default endpoints.algebra.Endpoints:
We start by fixing the type ClientErrors to ValidationError, which is used to model client errors. Then, we implement the method clientErrorsResponseEntity, which provides a description of response entities carrying client errors. This method is called at two places by the library: when such a client error occurs (e.g., a query parameter is missing), and when generating the OpenAPI documentation (so that the documentation is consistent with the actual responses).
The code shown above for EndpointsWithProblemDetailsErrors is incomplete. The full implementation also requires to fix the type used to model server errors (which is by default fixed to Throwable). You can read it here.
Once we have defined this trait, we can use it in place of endpoints.algebra.Endpoints to define our endpoint description:
The greet endpoint description hasn’t changed, it is the same as before. We have only swapped the default Endpoints algebra for our custom one, EndpointsWithProblemDetailsErrors, which uses ValidationError to represent client errors instead of Invalid.
What remains to be done is to provide two implementations for the currently abstract operation problemResponse. One in the context of a server, and one in the context of documentation.
The server implementation builds an HTTP response with content-type application/problem+json:
The documentation implementation “documents” an HTTP response with content-type application/problem+json:
Last, we put everything together by using these traits instead of the default server and documentation interpreters:
Now, our server returns Problem Details error entities (for both technical and business errors), and this change is reflected in the documentation:
You can find here a complete application based on this code: https://github.com/julienrf/endpoints-problemdetailserrors.
Implementing RESTful services comes with challenges, this article addresses two of them: keeping a uniform representation of technical and business errors, and reducing the cost of maintaining the documentation. The proposed solution uses the library endpoints, which derives servers, clients, and documentation from a single source of truth.