Deze referentie is alleen in het Engels. Naar het Nederlandse REST API-overzicht

Diese Referenz gibt es nur auf Englisch. Zur deutschen REST-API-Übersicht

Cette référence existe uniquement en anglais. Aller à la présentation de l'API REST en français

Esta referencia solo está en inglés. Ir a la introducción de la API REST en español

Concurrency and ETags

Concurrency and ETags

Use ETags to skip re-reading data that has not changed, and to stop a write from overwriting a change you have not seen.

View as Markdown

Every record or page you read or write comes with an ETag header. Send it back on your next request. With If-None-Match, a read returns no body when nothing changed. With If-Match, a write is refused when someone else changed the record first.

In the examples, <your-url> is the address you sign in to plnnrly on, such as acme.plnnrly.com, or your organisation's own domain.

ETag

There are two kinds of tag, and they answer different questions.

You readThe tagSend it back asNever as
One record, or the response to a writeStrong, such as "projects-12-t1791995400-3fa1"If-Match on a later write, or If-None-Match on a re-readNo restriction. Use the header that fits the request.
A page of a collectionWeak, such as W/"a1b2c3d4e5f6"If-None-Match on a re-read of the same pageIf-Match. A page is not a record.

A tag comes with every 200 or 201 that returns a record or a page, with a 304 and with a 412. A 204 from a DELETE has none, and neither does any other error.

Treat a tag as opaque. Store it exactly as you received it, quotes and W/ prefix included, and send it back unchanged. Do not parse it, and do not build one yourself.

Re-read only what changed: If-None-Match

Send the tag you hold. When nothing changed, the answer is 304 Not Modified with no body.

Terminal
curl -i "https://<your-url>/api/v2/projects/12" \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" \
  -H 'If-None-Match: "projects-12-t1791995400-3fa1"'
  • Repeat the exact query you used before. A record's tag also depends on the view and include you asked for: change either and you get a full 200. Other parameters, such as meta=actions, do not change the tag, so a 304 does not tell you whether meta.actions changed.
  • A page's tag is weak and covers the whole page as it was rendered. Send it back with its W/ prefix.
  • A 304 still uses one unit of your rate limit, whatever view or include you asked for. On a poller it is the cheapest way to ask "has this changed?".

Refuse a stale write: If-Match

Every PATCH, every DELETE and every action POST accepts If-Match. Send the tag of the version you based your change on. When the record changed since, nothing is written.

Terminal
curl -X PATCH "https://<your-url>/api/v2/projects/12" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "projects-12-t1791995400-3fa1"' \
  -d '{
    "status": "on-hold"
  }'

The 200 carries the record's new tag. Keep it for your next write.

  • If-Match is never required. Without it, the write goes ahead whatever changed in the meantime.
  • If-Match compares versions, not query shapes. A tag from any view or include of the current version is accepted.
  • If-Match: * matches any version, so it protects nothing. Send the real tag.

When the record changed: 412

Here a work item moved on to version 12 while you still held version 11:

Terminal
curl -X PATCH "https://<your-url>/api/v2/work-items/812" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "work-items-812-v11-8c2e"' \
  -d '{
    "title": "Fix the CSV import"
  }'

Nothing is written, and the answer is 412 precondition-failed. Its ETag header carries the current tag, "work-items-812-v12-8c2e", and the body gives the current version number:

JSON
{
  "type": "https://developers.plnnrly.com/api/v2/problems/precondition-failed",
  "title": "Precondition failed",
  "status": 412,
  "detail": "The record changed since the version named in If-Match. Re-read it and retry.",
  "instance": "/api/v2/work-items/812",
  "request_id": "01M4ZDAAR0K7Q3XW9B2NTC5H8E",
  "current_version": 12
}

current_version is a number for work items only. For every other resource it is null, because those records have no version counter.

To recover:

  1. Re-read the record, unless you already hold a copy with the tag from the 412.
  2. Re-apply your change to what is there now, and check that it still makes sense.
  3. Retry with the new tag.

Never resend the same write with the tag from the 412 without looking first. That overwrites exactly the change the lock caught.

How strong is the lock

  • Work items carry a version counter, version, so a write based on an older version is caught. Two writes sent at the same moment with the same tag can still both go through.
  • Every other resource is versioned by the second it was last changed. That is best effort: two writes in the same second share a tag, so If-Match cannot tell them apart. If two writers can change the same record within one second, coordinate those writes in your own code.
  • A tag tracks the record itself, not the rows you included. A change to an included record does not change the tag, so a 304 on a read with include can hide it. Read that record directly when you need it fresh.

Which header you want

You want to sayHeader
"Don't create this twice if my retry is a duplicate."Idempotency-Key on the POST
"Don't let me overwrite a change I haven't seen."If-Match on the PATCH, DELETE or action POST
"Don't send me a body I already have."If-None-Match on the GET

They are independent and combine. An action POST can carry both an Idempotency-Key and an If-Match.