Conditional requests

The ContentGrid REST API supports standard HTTP RFC9110 Conditional Requests.

The primary usage of conditional requests is to prevent the “lost update” problem, where one client overwrites the changes of another client that made a write between the read and write of the first client.

Without conditional request With conditional request
sequenceDiagram
    autonumber
    participant a as Client A
    participant s as ContentGrid API
    participant b as Client B

    a ->>+ s: GET item
    s -->>- a: 200 OK<br>ETag: "abc"<br>original data

    b ->>+ s: PUT item<br>new data
    s -->>- b: 204 No Content<br>ETag: "def"

    a ->>+ s: PUT item<br>other data
    rect hsl(0, 100%, 77.65%)
    s -->>- a: 204 No Content<br>ETag: "mno"
    note over a, s: "new data" was accidentally<br>overwritten by "other data"<br>without Client A having seen it
    end
sequenceDiagram
    autonumber
    participant a as Client A
    participant s as ContentGrid API
    participant b as Client B

    a ->>+ s: GET item
    s -->>- a: 200 OK<br>ETag: "abc"<br>original data

    b ->>+ s: PUT item<br>new data
    s -->>- b: 204 No Content<br>ETag: "def"

    a ->>+ s: PUT item<br>If-Match: "abc"<br>other data
    rect hsl(120, 100%, 78%)
    s -->>- a: 412 Precondition Failed
    note over a, s: request was rejected<br>Client A did not accidentally<br>overwrite the "new data"
    end

It is recommended to use conditional requests for all cases where there is an operation that first reads a resource and then makes changes to it based on what was read.

This recommendation is applicable to both human users and automated systems, for example:

  • Showing an edit form to a user (which first reads the entity-item to get the values to pre-fill into the form, and then writes when the user submits the edit form)
  • A webhook automation that adds tags based on the content stored in the entity-item

Supported resources

Conditional requests are supported for the following resources:

Name URL
entity-item /<entity-name-plural>/{id}
to-one relation /<entity-name-plural>/{id}/<relation-name>
entity-content /<entity-name-plural>/{id}/<attribute-name>

All methods on these resources support conditional requests.

Obtaining the ETag for a resource

Resources that support conditional requests return an ETag response header for read (GET/HEAD) and update (PUT/PATCH) operations. In the case of an update operation, the returned ETag is for the updated version of the resource and can immediately be used for a subsequent request. The DELETE operation does not return an ETag, because the resource no longer exists after it has finished.

The operations that are used to create such a resource (e.g. POST to the entity-collection) also return the ETag response header for the resource that was just created.

Note

ETags for a to-one relation are placed on the direct response to the relation endpoint, /<entity-name-plural>/{id}/<relation-name>, even though this is a redirect response.

The ETag that is received from the final response is the ETag for the entity-item that is on the other side of the relation. It can not be used for a conditional request to the relation itself.

ETag response for a to-one relation

​
curl -i -X GET https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/supplier   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID/supplier HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 302 Found
ETag: "913y2q8uowaisz3ydkjn453oa"
Location: /suppliers/ddda0e08-b72c-11f1-b32e-f4289d0500db
A client that follows redirects will automatically perform the following request:
​
curl -i -X GET https://$APP_ID.$REGION.contentgrid.cloud/suppliers/ddda0e08-b72c-11f1-b32e-f4289d0500db   \
    -H "Authorization: Bearer $TOKEN"
GET /suppliers/ddda0e08-b72c-11f1-b32e-f4289d0500db HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
ETag: "1mktvx6"
Content-Type: application/hal+json

{
  "id": "ddda0e08-b72c-11f1-b32e-f4289d0500db",
  [...]
}

The correct ETag for the supplier relation is the first one, "913y2q8uowaisz3ydkjn453oa"; the ETag "1mktvx6" is only valid for conditional requests to the supplier entity-item itself.

ETag response header for an entity-item
​
curl -i -X GET https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
ETag: "1e0k4j9"
Content-Type: application/hal+json

{
  "id": "019be613-baa4-7644-9438-e252be3bbe84",
  "received": "2024-07-15",
  "document": {
    "filename": "invoice.txt",
    "length": 13,
    "mimetype": "text/plain"
  },
  "pay_before": "2024-08-14",
  "total_amount": 15.95,
  "_links": {
    [...]
  }
}
ETag response header when creating an entity-item
​
curl -i -X POST https://$APP_ID.$REGION.contentgrid.cloud/invoices   \
    --json "{\"received\": \"2024-07-15\", \"total_amount\": 15.95, \"pay_before\": \"2024-08-14\"}"   \
    -H "Authorization: Bearer $TOKEN"
POST /invoices HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: application/json

{"received": "2024-07-15", "total_amount": 15.95, "pay_before": "2024-08-14"}
HTTP/1.1 201 Created
ETag: "ae1k4j6"
Location: /invoices/d2b7e47d-b5ae-11f1-8ed8-f4289d0500db
Content-Type: application/hal+json

{
  "id": "d2b7e47d-b5ae-11f1-8ed8-f4289d0500db",
  "received": "2024-07-15",
  "document": null,
  "pay_before": "2024-08-14",
  "total_amount": 15.95,
  "_links": {
    [...]
  }
}

Performing conditional requests

To perform conditional requests, a previously obtained ETag value has to be placed in the If-Match or If-None-Match headers.

Note that the quotes are part of the ETag value and should not be removed. For example: If-Match: "1e0k4j9".

Conditional requests are typically used in the following situations:

Methods Header Description
HEAD, GET If-Match: <etag> Only retrieve the resource when it has a specific ETag; else return 412 Precondition Failed
HEAD, GET If-None-Match: <etag> Only retrieve the resource when it has changed with regard to the specific ETag; else return 304 Not Modified. This can be used to update caches without needing to transfer the full resource when it has not changed.
PUT, PATCH, DELETE If-Match: <etag> Only change the resource when it has this specific ETag; else return 412 Precondition Failed
PUT If-None-Match: * Only create the resource when it does not exist (not for entity-item); else return 412 Precondition Failed

The If-Modified-Since and If-Unmodified-Since headers are not supported, because they are less precise, and some resources don’t store a last modified timestamp.

Using a conditional request for updating

In this example, we’re going to move the pay_before date one day earlier.

Using the ETag header from the invoice example above, we place it in the If-Match header for our next request. Then we perform a PATCH request to update only the pay_before attribute.

Between our requests…

To execute this example, you will need to change the If-Match header to the correct value yourself.

​
curl -i -X PATCH https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    --json "{\"pay_before\": \"2024-08-13\"}"   \
    -H 'If-Match: "1e0k4j9"'   \
    -H "Authorization: Bearer $TOKEN"
PATCH /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
If-Match: "1e0k4j9"
Content-Type: application/json

{"pay_before": "2024-08-13"}
HTTP/1.1 204 No Content
ETag: "i4xcj6"

The response to the modification also contains the new ETag value for the updated version of the object.

​
curl -i -X PATCH https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    --json "{\"pay_before\": \"2024-08-13\"}"   \
    -H 'If-Match: "1e0k4j9"'   \
    -H "Authorization: Bearer $TOKEN"
PATCH /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
If-Match: "1e0k4j9"
Content-Type: application/json

{"pay_before": "2024-08-13"}
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json

{
  "type": "https://contentgrid.cloud/problems/unsatisfied-version",
  "title": "Object has changed",
  "detail": "Requested version constraint 'is any of [exactly '1e0k4j9']' can not be satisfied (actual version exactly '1r061qt')",
  "status": 412,
  "actual_version": "1r061qt"
}

More details about the error response format can be found in the error handling section.