In a REST application, it’s often the case that several clients might interact with a single resource, each holding a copy of the resources state. At any point in time, these client’s understanding of resource state may differ from each other or from the server. Without some way of realigning resource state, changes requested by a client based on an out-of-date understanding of resource may have undesired effects, from repeating computationally expensive requests to overwriting and losing another client’s changes.
Thankfully, the HTTP specification provides a simple mechanism for aligning resource state, and preventing race conditions. This mechanism is provided by the combination of entity tags and conditional requests.
An entity tag, specified by the
ETag HTTP header, is an opaque token
that the server associates with a particular state of a resource. Whenever
the resource state changes, the entity tag should be changed accordingly.
ETag value can be used by clients and servers to determine if
a request to a resource is up-to-date by comparing the value of the
header on the incoming request, to the value of the
ETag header present
on the server. If the values match, the client request is using an
up-to-date representation of the resource. If the values do not match, the
client should refresh its copy of the resource to make sure it is
A conditional request is a request that may be executed differently depending on the value of specific HTTP headers. These headers define the precondition that must be true before the server should execute the request. With respect to entity tags, we have two options for making requests conditional.
- If-Match: The request will succeed if the ETag of the remote resource is equal to the one listed in this header.
- If-None-Match: The request will succeed if the ETag of the remote resource is different to each listed in this header.
By specifying the appropriate
ETag and condition header, you can perform
optimistic locking for concurrent operations on a resource. Let’s walk
through an example of how this works in practice.
Consider the use case of updating a remote resource using HTTP. As an example, imagine you want to modify some data in a spreadsheet. First, the client uses a GET request to fetch a copy of the document, makes local modifications, then issues a PUT request to update the resource on the server.
With a single client, this interaction happens smoothly. Unfortunately, once we have more than one client we need to take into account concurrency. While client A is modifying its local copy of a resource, client B can fetch the same resource and modify its local copy. If both clients then attempt to PUT their modifications back to the server, the modifications of the first client to PUT are lost when the second client to PUT overwrites them. And neither client is aware of the situation. The decision about who’s changes are kept depends on the performance of the clients, the network, the server, or any number of factors. This is a race condition that leads to difficult to detect bugs and unexpected behaviour.
To avoid this race condition, we need a way to notify client’s of conflicts. Conditional requests allow us to implement an optimistic locking algorithm to avoid race conditions. With optimistic locking, each client is able to make local changes to a resource, and the client is notified of conflicts when those changes are rejected by server when an update is attempted.
In this example, we implement optimistic locking using the
header. If the
ETag header does not match the value of the resource on
the server, the server rejects the change with a
412 Precondition Failed
error. The client is therefore notified of the error, and can try the
request again after updating their local copy of the resource.
Conditional requests are a key feature of HTTP and allow us to build efficient and complex applications using optimistic locking. They are a fundamental feature of the Web that, if used properly, allow you to build scalable and performant distributed applications.