Enterprise Java

REST API Design: Dealing with concurrent updates

Concurrency control can be an important part of a REST API, especially if you expect concurrent update requests for the same resource. In this post we will look at different options to avoid lost updates over HTTP.

Let’s start with an example request flow, to understand the problem:

We start with Alice and Bob requesting the resource /articles/123 from the server which responds with the current resource state. Then, Bob executes an update request based on the previously received data. Shorty after that, Alice also executes an update request. Alice’s request is also based on the previously received resource and does not include the changes made by Bob. After the server finished processing Alice’s update Bob’s changes have been lost.

HTTP provides a solution for this problem: Conditional requests, defined in RFC 7232.

Conditional requests use validators and preconditions defined in specific headers. Validators are metadata generated by the server that can be used to define preconditions. For example, last modification dates or ETags are validators that can be used for preconditions. Based on those preconditions the server can decide if an update request should be executed.

For state changing requests the If-Unmodified-Since and If-Match headers are particularly interesting. We will learn how to avoid concurrent updates using those headers in the next sections.

Using a last modification date with an If-Unmodified-Since header

Probably the easiest way to avoid lost updates is the use of a last modification date. Saving the date of last modification for a resource is often a good idea so it is likely we already have this value in our database. If this is not the case, it is often very easy to add.

When returning a response to the client we can now add the last modification date in the Last-Modified response header. The Last-Modified header uses the following format:

<day-name>, <day> <month-name> <year> <hour>:<minute>:<second> GMT

For example:

Request:

GET /articles/123

Response:

HTTP/1.1 200 OK
Last-Modified: Sat, 13 Feb 2021 12:34:56 GMT

{
    "title": "Sunny summer",
    "text": "bla bla ..."
}

To update this resource the client now has to add the If-Unmodified-Since header to the request. The value of this header is set to the last modification date retrieved from the previous GET request.

Example update request:

PUT /articles/123
If-Unmodified-Since: Sat, 13 Feb 2021 12:34:56 GMT

{
    "title": "Sunny winter",
    "text": "bla bla ..."
}

Before executing the update, the server has to compare the last modification date of the resource with the value from the If-Unmodified-Since header. The update is only executed if both values are identical.

One might argue that it is enough to check if the last modification date of the resource is newer than the value of the If-Unmodified-Since header. However, this gives clients the option to overrule other concurrent requests by sending a modified last modification date (e.g. a future date).

A problem with this approach is that the precision of the Last-Modified header is limited to seconds. If multiple concurrent update requests are executed in the same second, we can still run into the lost update problem.

Using an ETag with an If-Match header

Another approach is the use of an entity tag (ETag). ETags are opaque strings generated by the server for the requested resource representation. For example, the hash of the resource representation can be used as ETag.

ETags are sent to the client using the ETag Header. For example:

Request:

GET /articles/123

Response:

HTTP/1.1 200 OK
ETag: "a915ecb02a9136f8cfc0c2c5b2129c4b"

{
    "title": "Sunny summer",
    "text": "bla bla ..."
}

When updating the resource, the client sends the ETag header back to the server:

PUT /articles/123
ETag: "a915ecb02a9136f8cfc0c2c5b2129c4b"

{
    "title": "Sunny winter",
    "text": "bla bla ..."
}

The server now verifies that the ETag header matches the current representation of the resource. If the ETag does not match, the resource state on the server has been changed between GET and PUT requests.

Strong and weak validation

RFC 7232 differentiates between weak and strong validation:

Weak validators are easy to generate but are far less useful for comparisons. Strong validators are ideal for comparisons but can be very difficult (and occasionally impossible) to generate efficiently.

Strong validators change whenever a resource representation changes. In contrast weak validators do not change every time the resource representation changes.

ETags can be generated in weak and strong variants. Weak ETags must be prefixed by W/.

Here are a few example ETags:

Weak ETags:

ETag: W/"abcd"
ETag: W/"123"

Strong ETags:

ETag: "a915ecb02a9136f8cfc0c2c5b2129c4b"
ETag: "ngl7Kfe73Mta"

Besides concurrency control, preconditions are often used for caching and bandwidth reduction. In these situations weak validators can be good enough. For concurrency control in REST APIs strong validators are usually preferable.

Note that using Last-Modified and If-Unmodified-Since headers is considered weak because of the limited precision. We cannot be sure that the server state has been changed by another request in the same second. However, it depends on the number of concurrent update requests you expect if this is an actual problem.

Computing ETags

Strong ETags have to be unique for all versions of all representations for a particular resource. For example, JSON and XML representations of the same resource should have different ETags.

Generating and validating strong ETags can be a bit tricky. For example, assume we generate an ETag by hashing a JSON representation of a resource before sending it to the client. To validate the ETag for an update request we now have to load the resource, convert it to JSON and then hash the JSON representation.

In the best case resources contain an implementation-specific field that tracks changes. This can be a precise last modification date or some form of internal revision number. For example, when using database frameworks like Java Persistence API (JPA) with optimistic locking we might already have a version field that increases with every change.

We can then compute an ETag by hashing the resource id, the media-type (e.g. application/json) together with the last modification date or the revision number.

HTTP status codes and execution order

When working with preconditions, two HTTP status codes are relevant:

  • 412 – Precondition failed indicates that one or more preconditions evaluated to false on the server (e.g. because the resource state has been changed on the server)
  • 428 – Precondition required has been added in RFC 6585 and indicates that the server requires the request to be conditional. The server should return this status code if an update request does not contain a expected preconditions

RFC 7232 also defines the evaluation order for HTTP 412 (Precondition failed):

[..] a recipient cache or origin server MUST evaluate received request preconditions after it has successfully performed its normal request checks and just before it would perform the action associated with the request method.  A server MUST ignore all received preconditions if its response to the same request without those conditions would have been a status code other than a 2xx (Successful) or 412 (Precondition Failed).  In other words, redirects and failures take precedence over the evaluation of preconditions in conditional requests.

This usually results in the following processing order of an update request:

Before evaluating preconditions, we check if the request fulfills all other requirements. When this is not the case, we respond with a standard 4xx status code. This way we make sure that other errors are not suppressed by the 412 status code.

Published on Java Code Geeks with permission by Michael Scharhag, partner at our JCG program. See the original article here: REST API Design: Dealing with concurrent updates

Opinions expressed by Java Code Geeks contributors are their own.

Michael Scharhag

Michael Scharhag is a Java Developer, Blogger and technology enthusiast. Particularly interested in Java related technologies including Java EE, Spring, Groovy and Grails.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button