Beyond Basic REST - Deep Dive into ETags and If-Match

Beyond Basic REST - Deep Dive into ETags and If-Match

Sunny Sun Lv4

Optimizing Concurrency Handling with ETag and If-Match

** This post is updated on 26-07-2024

While the underlying structure of RESTful APIs is crucial, the true magic lies in the details – specifically, HTTP headers. Among these unsung heroes, ETag and If-Match/If-None-Match stand out for their ability to optimize performance and ensure data consistency.

In this post, we’ll delve into these powerful headers and explore how to leverage them effectively within NestJS. By mastering their implementation, we’ll equip the RESTful API to handle concurrent transactions with grace and ensure robust data management.

Understanding HTTP Headers

Before diving into ETags and If-Match/If-None-Match, it’s essential to understand HTTP headers.
HTTP headers are key-value pairs that carry additional information with an HTTP request or response. They provide context about the request or response, enabling efficient communication between the client and server.

Types of HTTP Headers

  • General Headers: Apply to both requests and responses, providing general information about the message.
    • Examples: Cache-Control
  • Request Headers: Convey information about the client and the request being made.
    • Examples: Accept, Authorization, User-Agent
  • Response Headers: Provide information about the response, including content type, status, and caching directives.
    • Examples: Content-Type, Content-Length, Set-Cookie
  • Entity Headers: Describe the characteristics of the message body.
    • Examples: Etag, Content-Encoding

What is ETag and If-Match

An Entity Tag, or ETag, is used to identify a specific version of a resource uniquely. It is a hash or identifier associated with the state of a resource on the server. There are two types of Etag: weak and strong . The weak Etag allows for flexibility in representing semantically equivalent content. Strong Etag is suitable for the resource representation that is byte-for-byte identical for all users.

When a client requests a resource, the server attaches an ETag to the response. Subsequent requests from the client can include this ETag in the If-None-Match header. The header is used to determine whether the resource has changed, the server responds with a “304 Not Modified” status if the resource has not changed, reducing unnecessary data transfer and enhancing performance.

On the other hand, the If-Match header is employed for optimistic concurrency control. It can ensure that an operation, such as an update or deletion, is only performed if the provided ETag matches the current state of the resource on the server.

The significance of ETag and If-Match lies in their ability to prevent conflicting updates. In scenarios where multiple clients modify the same resource concurrently, these headers act as safeguards. ETag enables efficient caching and reduces unnecessary data transfers while If-Match ensuring that updates occur only when the client possesses the latest resource version. Together, they contribute to a more resilient interaction between clients and servers in RESTful APIs.

How to create and return an Etag

To generate an ETag in NestJS using the [etag](https://github.com/jshttp/etag) library, follow the below steps to install and import the library.

1
2
3
4
// install it  
npm install etag
// import it
import * as etag from 'etag';

Then, we can generate an Etag with a one-line call.

1
2
const data = // your data here;  
const generatedETag = etag(data);

Please note that the etag function only accepts string, buffer, or steams. We can’t pass objects or arrays directly, but a workaround exists below.

1
2
3
4
5
const arrayData = [1, 2, 3];  
const etagForArray = etag(JSON.stringify(arrayData));

const objectData = { key: 'value' };
const etagForObject = etag(JSON.stringify(objectData));

Then, we can return the Etag in the response header for a GET request.

1
2
3
4
5
6
7
8
9
10
@Get('etag')  
async GetResource(@Res({ passthrough: true }) res: Response) {
// fetching resource data
const resourceData = await this.fetchResourceData();

// Attach ETag to the response header
res.set('ETag', etag(JSON.stringify(resourceData)));

return resourceData;
}

Please note that we need to set the passthrough option to true in the @Res({ passthrough: true }) decorator because injecting the @Res will disable the default route handling by default.

Use Etag for effective Caching

One of the primary purposes of Etag is caching. After calling the GET the first time, the client retrieves the Etag in response, and then the subsequent requests can include this ETag in the If-None-Match header.

an example of if-none-match header in the request

If-None-Match: “bfc13a64729c4290ef5b2c2730249c88ca92d82d”

In the GET endpoint, add a check to compare the Etag in If-None-Match to determine whether a resource has been modified. We can return a 304 Not Modified response if the resource has not been changed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Get()  
async getResource( @Res({ passthrough: true }) res: Response,
@Headers('if-none-match') ifNoneMatch: string, ){
// fetching the resource data
const resourceData = await this.fetchResourceData();

// Check If-None-Match header to determine if the resource has changed
if (ifNoneMatch && ifNoneMatch === etag(JSON.stringify(resourceData))) {
// Resource has not changed, return 304 Not Modified
res.status(HttpStatus.NOT_MODIFIED).send();
return;
}

// Return the resource data with the current ETag
}

Upon receiving the response with a 304 status, most modern browsers will fetch the resource from the local cache.

Use If-Match for Optimistic Concurrency Control

The If-Match header is commonly used to facilitate optimistic concurrency control. What exactly is optimistic concurrency control?

Optimistic concurrency control is a strategy for managing multiple users attempting to modify the same piece of data simultaneously. Instead of locking the data and preventing others from making changes, optimistic concurrency assumes that conflicts are rare. Users can make changes independently, but before saving their modifications, the system checks if someone else has modified the data. If no changes conflict, the modifications are accepted; otherwise, the system prompts users to resolve the inconsistency.

Now, let’s see how to implement it in a PUT request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 @Put(':id')  
async updateResource(
@Res() res: Response,
@Param('id') id: string,
@Body() updateData: any,
@Headers('if-match') ifMatch: string, // Extract If-Match header
): Promise<void> {
// Simulate fetching the resource data from the database
const currentResourceData = await this.getResourceData(id);
const currentEtag = etag(JSON.stringify(currentResourceData));

// Validate If-Match header against the current ETag
if (ifMatch.toString() !== currentEtag.toString()) {
// ETag mismatch, return Precondition Failed status
res.status(HttpStatus.PRECONDITION_FAILED).send();
return;
}

...
}

When a client sends a request to update a resource, the client includes the current ETag of the resource in the If-Match header. The server then checks if the provided ETag matches the current state of the resource. If there’s a match, the update proceeds; otherwise, the server returns a “412 Precondition Failed” status, indicating that another party has modified the resource.

The same approach can be used not only for PUT but also applicable to DELETE and PATCH requests.

What is difference between If-Match and If-None-Match

It is worth highlighting that If-Match and If-None-Match headers serve different purposes in the context of ETags. Here’s a breakdown of their differences:

If-Match Header

  • It is used in requests to operate (e.g., update or delete) only if the provided ETag matches the current ETag of the resource on the server.
  • If the ETag matches, the operation is performed; otherwise, the server responds with a “412 Precondition Failed” status, indicating that another party modified the resource.

If-None-Match Header

  • It is used in requests to get a resource only if its ETag does not match the specified ETag(s).
  • If the ETag matches, the server responds with a “304 Not Modified” status, indicating that the client’s cached version is still valid and there’s no need to transfer the resource again.

Summary

We explored the use of ETag and If-Match/If-None-Match headers. Using ETag with If-Match is a good practice for optimistic concurrency control. I hope this post has been helpful and that you’ve learned something new.

  • Title: Beyond Basic REST - Deep Dive into ETags and If-Match
  • Author: Sunny Sun
  • Created at : 2024-03-05 00:00:00
  • Updated at : 2024-07-27 15:08:53
  • Link: http://coffeethinkcode.com/2024/03/05/beyond-basic-rest-deep-dive-etags-if-match/
  • License: This work is licensed under CC BY-NC-SA 4.0.