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
|
Recommended usage
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 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.
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.