RESTful error handling with Akka HTTP and the library “endpoints”
January 29, 2020 | Technical Blog
Some resources of our system can be modified by several clients. Consider the situation where two clients are working on the same resource, what happens if both clients attempt to update the resource at the same time?
Here is a diagram to illustrate the situation:
One way to sort this situation out is to forbid concurrent updates: if the resource you want to update has already been modified since you last read it, your update is rejected. In our example, Bob’s update at step 4 would have been rejected because Alice modified the resource after Bob had read it.
This mechanism is called optimistic concurrency control. We can implement it by versioning the resources and requiring that a version is passed along with the update. We can use a simple version number assigned to 1 upon creation and incremented by each subsequent modification of the resource. When a client updates a resource, it also indicates the version it is working with. The server can thus validate whether or not the resource has been modified in the meantime. The diagram below shows how this would lead to rejecting Bob’s update:
In practice, how do we handle the version of resources exposed over HTTP? it turns out that the HTTP protocol does have headers for this purpose. Indeed, a response containing a resource can use the ETag header to indicate its version. Conversely, a request updating a resource can use the If-Match header to indicate the last known version of the resource.
The system we want to implement has two endpoints, one for reading the current state of a resource, and one for updating the state of the resource:
The endpoint for reading the resource returns the resource content, and its current version number in the
The endpoint for updating the resource takes the new resource content in the request entity, and the last known resource version in the
If-Match request header. If the update is successful, the response status is
OK (200), the response entity contains the updated resource, and the new resource version is provided in the
ETag header. In case of failure, the response status is
Precondition Failed (412) and the response is empty.
To implement this system with endpoints4s, we first need to write a description of the endpoints, from which, we can derive the server and client implementations, as well as the OpenAPI documentation. The library endpoints4s provides building blocks for describing endpoints, our first step consists in using these building blocks to describe the particular aspects of the endpoints we are interested in, such as “an
ETag response header carrying a version”, or “a response that could be either
OK (200) or
Precondition Failed (412)”.
Let’s start with the description of an ETag response header containing the version of a resource. A first attempt is the following:
We create a trait VersionedEndpoints that enriches the base trait endpoints4s.algebra.Endpoints with a value describing the ETag response header. However, there is a problem with this definition: we get the raw content of the header, as a String. We can improve it by parsing the content of the header to extract just the version. To make it easier to distinguish between a String value that contains a raw ETag value and a String value that contains a parsed version, we also introduce a type Version:
xmap operation transforms the
ResponseHeader[String] into a
ResponseHeader[Version] by using the two functions,
Our implementation is incomplete because it doesn’t support “weak” ETag values, but it is sufficient for the purpose of this article.
Similarly, we add the following definition to the
VersionedEndpoints trait to describe an
If-Match request header containing a version:
The next step is to describe an
OK (200) response whose entity contains a JSON representation of the resource, and whose
ETag header contains its version:
Since we want to represent our resource as JSON, we update the definition of the trait
VersionedEndpoints to also mix in the
JsonEntitiesFromSchemas base trait. The method
versionedResponse describes a response with the status code
OK, carrying a JSON entity and an
To avoid manipulating a pair
(A, Version), we create a type
Finally, we describe a response that can be either
OK (200) or
Precondition Failed (412):
Note that the result type of
Response[Either[Unit, Versioned[A]]], which means “a response containing either no value (in case of update failure), or the versioned resource”. An equivalent but slightly more practical way to model the response result would be to use the type
None represents an update failure). We can achieve this by using
Now we can put everything together to describe the endpoints for reading and updating a resource:
As specified at the beginning of the article, the endpoint for reading the resource uses the
GET verb and the
/resource URL , the response contains the JSON representation of the resource in the entity, and the resource version in the
The endpoint for updating the resource uses the
PUT verb and the
/resource URL , the request entity contains the new resource content, and the
If-Match request header contains the last version of the resource known to the client. The response is either
Precondition Failed (412) or
OK (200). In the latter case, the response entity contains the updated resource content, and the
ETag response header contains the new resource version.
To implement a server for these endpoints, we apply a server interpreter to the endpoint descriptions and provide their business logic. The logic for the
read endpoint consists of reading the current state of the resource from the database, and the logic for the
update endpoint consists of updating the state of the resource in the database.
Let’s assume we have a database with the following API:
This API would be easy to implement with slick-repo, but this is left as an exercise to the reader.
So, assuming we have an implementation for our
Database trait, we can implement a server for our endpoints:
In this example, we apply an Akka-HTTP server interpreter by mixing the traits that are in the package
endpoints4s.akkahttp.server to our trait
ResourcesApi, which contains the description of the endpoints.
We use the operation
implementedByAsync on the endpoints to supply their logic, which forwards to the database API. As a result, we get good old Akka-HTTP routes.
The server interpreter makes sure that incoming requests are well formed. For instance, if the
If-Match header is missing from an update request, the server returns a
Bad Request (400) response with a sensible error message.
Implementing a client consists of mixing a client interpreter to the trait
ResourcesApi. For instance, to use a client based on Akka-HTTP:
And we are done. We can use the client to test the scenario presented in the introduction of the article:
Under the hood, the client sends HTTP requests according to the description we have written above, these requests are handled by the server, which returns HTTP responses, that are eventually decoded by the client.
To make our service usable for our partners, we publish an OpenAPI document for it. We can get an OpenAPI document for our service by applying an interpreter to our endpoint descriptions:
The OpenAPI interpreter provides an operation
openApi that takes the endpoints to document as parameters (we pass
update) and it returns an
OpenApi value containing the documentation, which we can serialize into JSON by using the serializer provided by endpoints4s.
The resulting document can be processed by tools like SwaggerUI or ReDoc to build developer portals. For instance, by adding more documentation information to our endpoint descriptions we can generate the following documentation with ReDoc:
For reference, here is the complete endpoint description:
The complete source code is available here: https://github.com/julienrf/optimistic-concurrency-control-http.
Optimistic concurrency control is a solution for handling concurrent updates applied to the same resource without having to resort to a locking system.
We have shown how to embrace the HTTP protocol to implement optimistic concurrency control for resources exposed on REST APIs. Our implementation guarantees that servers, clients, and documentation are consistent together. Furthermore, it provides a clean separation between low-level concerns related to the HTTP protocol, and high-level business concerns.