Exploring Etag and If-Match in NestJS

Exploring Etag and If-Match in NestJS

Sunny Sun Lv4

Optimizing Concurrency Handling with ETag and If-Match

HTTP headers play an important role in building an efficient RESTful API. Two such headers, ETag and If-Match/If-None-Match, are instrumental in handling concurrency and caching in RESTful APIs.

In this post, we will delve into Etag and If-Match/If-None-Match headers, and explore how to use them in NestJS. Correctly using them will allow us to design a robust RESTful API capable of handling concurrent transactions.

Understanding 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.

Generate 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 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.

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.

If-Match vs 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.

We discussed the usage of Etag and If-match/if-none-match in this post. In many cases, it is a good practice to use a combination of etag and if-match for optimistic concurrency control. I hope you have learned one thing or two in this post.

  • Title: Exploring Etag and If-Match in NestJS
  • Author: Sunny Sun
  • Created at : 2024-03-05 00:00:00
  • Updated at : 2024-07-15 19:37:14
  • Link: http://coffeethinkcode.com/2024/03/05/exploring-etag/
  • License: This work is licensed under CC BY-NC-SA 4.0.