Edit Page

ETag

🔧 Configuration

Sets localhost:8080 with admin:secret
Values are saved in your browser

The ETag or entity tag is part of HTTP; it is used for:

  • Web cache validation, which allows a client to make conditional requests. This allows caches to be more efficient, and saves bandwidth, as a web server does not need to send a full response if the content has not changed.

  • For optimistic concurrency control as a way to help prevent ghost writes, i.e. simultaneous updates of a resource from overwriting each other.

RESTHeart automatically manages ETags, creating and updating them.

For example, let’s create a database, a collection and a document. You can note that any response includes the header ETag (other response headers are omitted for simplicity) and this is valid for any type of resource, included file resources.

cURL

curl -i -X PUT '[INSTANCE-URL]/test' \
  -H 'Content-Type: application/json' \
  -u '[BASIC-AUTH]' \
  -d '{"descr": "a db for testing"}'

HTTPie

http PUT '[INSTANCE-URL]/test' \
  Content-Type:application/json \
  --auth '[BASIC-AUTH]' \
  descr="a db for testing"

JavaScript

fetch('[INSTANCE-URL]/test', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Basic [BASIC-AUTH]'
  },
  body: JSON.stringify({"descr": "a db for testing"})
})
.then(response => {
  if (response.ok) {
    console.log('Database created successfully');
  } else {
    console.error('Failed to create database:', response.status);
  }
})
.catch(error => console.error('Error:', error));

HTTP/1.1 201 Created ETag: 55e84b95c2e66d1e0a8e46b2 (other headers omitted)

cURL

curl -i -X PUT '[INSTANCE-URL]/test/coll' \
  -H 'Content-Type: application/json' \
  -u '[BASIC-AUTH]' \
  -d '{"descr": "a collection for testing"}'

HTTPie

http PUT '[INSTANCE-URL]/test/coll' \
  Content-Type:application/json \
  --auth '[BASIC-AUTH]' \
  descr="a collection for testing"

JavaScript

fetch('[INSTANCE-URL]/test/coll', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Basic [BASIC-AUTH]'
  },
  body: JSON.stringify({"descr": "a collection for testing"})
})
.then(response => {
  if (response.ok) {
    console.log('Collection created successfully');
  } else {
    console.error('Failed to create collection:', response.status);
  }
})
.catch(error => console.error('Error:', error));

HTTP/1.1 201 Created ETag: 55e84be2c2e66d1e0a8e46b3 (other headers omitted)

cURL

curl -i -X PUT '[INSTANCE-URL]/test/coll/doc' \
  -H 'Content-Type: application/json' \
  -u '[BASIC-AUTH]' \
  -d '{"descr": "a document for testing"}'

HTTPie

http PUT '[INSTANCE-URL]/test/coll/doc' \
  Content-Type:application/json \
  --auth '[BASIC-AUTH]' \
  descr="a document for testing"

JavaScript

fetch('[INSTANCE-URL]/test/coll/doc', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Basic [BASIC-AUTH]'
  },
  body: JSON.stringify({"descr": "a document for testing"})
})
.then(response => {
  if (response.ok) {
    console.log('Document created successfully');
  } else {
    console.error('Failed to create document:', response.status);
  }
})
.catch(error => console.error('Error:', error));

HTTP/1.1 201 Created ETag: 55e84c0ac2e66d1e0a8e46b4 (other headers omitted)

cURL

curl -i -X GET '[INSTANCE-URL]/test/coll/doc' \
  -u '[BASIC-AUTH]'

HTTPie

http GET '[INSTANCE-URL]/test/coll/doc' \
  --auth '[BASIC-AUTH]'

JavaScript

fetch('[INSTANCE-URL]/test/coll/doc', {
  method: 'GET',
  headers: {
    'Authorization': 'Basic [BASIC-AUTH]'
  }
})
.then(response => response.json())
.then(data => {
  console.log('Retrieved document:', data);
})
.catch(error => console.error('Error:', error));

HTTP/1.1 200 OK ETag: 55e84c0ac2e66d1e0a8e46b4 (other headers omitted)

{ "descr": "a document for testing" }

ETag for write requests

The checking policy is configurable and the default policy only requires the ETag for DELETE /db and DELETE /db/collection requests.

Previous versions always require the ETag to be specified for any write request.

Let’s try to update the document at URI /test/coll/doc forcing the ETag check with the checkEtag query parameter.

cURL

curl -i -X PUT '[INSTANCE-URL]/test/coll/doc?checkEtag' \
  -H 'Content-Type: application/json' \
  -u '[BASIC-AUTH]' \
  -d '{"descry": "a document for testing but modified"}'

HTTPie

http PUT '[INSTANCE-URL]/test/coll/doc?checkEtag' \
  Content-Type:application/json \
  --auth '[BASIC-AUTH]' \
  descry="a document for testing but modified"

JavaScript

fetch('[INSTANCE-URL]/test/coll/doc?checkEtag', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Basic [BASIC-AUTH]'
  },
  body: JSON.stringify({"descry": "a document for testing but modified"})
})
.then(response => {
  if (response.ok) {
    console.log('Document updated successfully');
  } else {
    console.error('Failed to update document:', response.status);
  }
})
.catch(error => console.error('Error:', error));

HTTP/1.1 409 Conflict …​ ETag: 55e84c0ac2e66d1e0a8e46b4

RESTHeart send back the error message 409 Conflict, showing that the document has not been updated.

Note that the ETag header is present in the response.

Let’s try to pass now a wrong ETag via the If-Match request header

cURL

curl -i -X PUT '[INSTANCE-URL]/test/coll/doc?checkEtag' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: [ETAG-VALUE]' \
  -u '[BASIC-AUTH]' \
  -d '{"desc": "a document for testing but modified"}'

HTTPie

http PUT '[INSTANCE-URL]/test/coll/doc?checkEtag' \
  Content-Type:application/json \
  If-Match:'[ETAG-VALUE]' \
  --auth '[BASIC-AUTH]' \
  desc="a document for testing but modified"

JavaScript

fetch('[INSTANCE-URL]/test/coll/doc?checkEtag', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'If-Match': '[ETAG-VALUE]',
    'Authorization': 'Basic [BASIC-AUTH]'
  },
  body: JSON.stringify({"desc": "a document for testing but modified"})
})
.then(response => {
  if (response.ok) {
    console.log('Document updated successfully');
  } else {
    console.error('Failed to update document:', response.status);
  }
})
.catch(error => console.error('Error:', error));

HTTP/1.1 412 Precondition Failed …​ ETag: 55e84c0ac2e66d1e0a8e46b4

RESTHeart send back the error message 412 Precondition Failed, showing that the document has not been updated.

Again the correct ETag header is present in the response.

Let’s try to pass now the correct ETag via the If-Match request header

cURL

curl -i -X PUT '[INSTANCE-URL]/test/coll/doc?checkEtag' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: [ETAG-VALUE]' \
  -u '[BASIC-AUTH]' \
  -d '{"descr": "a document for testing but modified"}'

HTTPie

http PUT '[INSTANCE-URL]/test/coll/doc?checkEtag' \
  Content-Type:application/json \
  If-Match:'[ETAG-VALUE]' \
  --auth '[BASIC-AUTH]' \
  descr="a document for testing but modified"

JavaScript

fetch('[INSTANCE-URL]/test/coll/doc?checkEtag', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'If-Match': '[ETAG-VALUE]',
    'Authorization': 'Basic [BASIC-AUTH]'
  },
  body: JSON.stringify({"descr": "a document for testing but modified"})
})
.then(response => {
  if (response.ok) {
    console.log('Document updated successfully with correct ETag');
  } else {
    console.error('Failed to update document:', response.status);
  }
})
.catch(error => console.error('Error:', error));

HTTP/1.1 200 OK ETag: 55e84f5ac2e66d1e0a8e46b8 (other headers omitted)

Yes, updated! And the response includes the new ETag value.

ETag for web caching

The responses of GET requests on document and file resources always include the ETag header.

The ETag is used by browsers for caching: after the first data retrieval, the browser will send further requests with If-None-Match request header. In case the resource state has not been modified (leading to a change in the ETag value), the response will be just 304 Not Modified, without passing back the data and thus saving bandwidth. This is especially useful for file resources.

cURL

curl -i -X GET '[INSTANCE-URL]/test/coll/doc' \
  -u '[BASIC-AUTH]'

HTTPie

http GET '[INSTANCE-URL]/test/coll/doc' \
  --auth '[BASIC-AUTH]'

JavaScript

fetch('[INSTANCE-URL]/test/coll/doc', {
  method: 'GET',
  headers: {
    'Authorization': 'Basic [BASIC-AUTH]'
  }
})
.then(response => response.json())
.then(data => {
  console.log('Retrieved document (web caching example):', data);
})
.catch(error => console.error('Error:', error));

HTTP/1.1 200 OK ETag: 55e84c0ac2e66d1e0a8e46b4 (other headers omitted)

{"descr": "a document for testing but modified"}

cURL

curl -i -X GET '[INSTANCE-URL]/test/coll/doc' \
  -H 'If-None-Match: [ETAG-VALUE]' \
  -u '[BASIC-AUTH]'

HTTPie

http GET '[INSTANCE-URL]/test/coll/doc' \
  If-None-Match:'[ETAG-VALUE]' \
  --auth '[BASIC-AUTH]'

JavaScript

fetch('[INSTANCE-URL]/test/coll/doc', {
  method: 'GET',
  headers: {
    'If-None-Match': '[ETAG-VALUE]',
    'Authorization': 'Basic [BASIC-AUTH]'
  }
})
.then(response => {
  if (response.status === 304) {
    console.log('Not Modified');
  } else {
    return response.json();
  }
})
.then(data => data && console.log(data));

HTTP/1.1 304 Not Modified

ETag policy

RESTHeart has a default configurable ETag checking policy.

The following configuration file snippet defines the default ETag check policy:

  • The policy applies for databases, collections (also applies to file buckets) and documents.

  • valid values are REQUIRED, REQUIRED_FOR_DELETE, OPTIONAL

It defines when the ETag check is mandatory.

etag-check-policy:
    db: REQUIRED_FOR_DELETE
    coll: REQUIRED_FOR_DELETE
    doc: OPTIONAL

The ETag checking policy can also be modified at request level with the checkETag query parameter and at db or collection level using the etagPolicy and etagDocPolicy metadata.

For instance specifying the following collection metadata, the ETag will be checked for all write requests on the collection resources and its documents.

{
  "etagPolicy": "REQUIRED",
  "etagDocPolicy": "REQUIRED"
}