Egy szerkesztőfelületben két ember megnyitja ugyanazt a rekordot. Az első elmenti a módosítását, a második pedig a régi űrlappal ráküldi a sajátját. Ha a PUT végpont feltétel nélkül ír, az első módosítás csendben eltűnik. Ez a lost update probléma, és nem kell hozzá több szerver vagy különösen nagy terhelés: elég két böngészőfül.

Erre nem muszáj saját version mezőt és egyedi fejlécet kitalálni. A HTTP ETag validátora és az If-Match kérésfejléc együtt pontosan erre való: a mentést ahhoz köti, hogy a kliens valóban azt az állapotot szerkesztette-e, amely még most is aktuális.
A cache-validátorból konkurenciavédelem
Az ETag-et többnyire a 304 Not Modified kapcsán tanuljuk. A szerver a válasz reprezentációjához küld egy idézőjeles, átlátszatlan azonosítót, például ETag: "17". Később a kliens ezzel kérdez rá, változott-e az adat. Az ETag jelentését a szerver adja: lehet tartalomhash, adatbázis-revízió vagy más, a reprezentációt azonosító érték. A HTTP specifikáció szerint nem dátum, hanem opaque validator, ezért a kliensnek nem szabad értelmeznie vagy újragyártania.
Íráskor a kliens az előző GET válasz ETag-jét küldi vissza If-Match alatt. Ha az érték még egyezik az aktuális ETag-gel, a szerver végrehajthatja a PUT, PATCH vagy DELETE műveletet. Ha már nem egyezik, 412 Precondition Failed a helyes válasz: a kliens elavult állapotból dolgozott. Az If-Match definíciója kifejezetten a párhuzamos írók véletlen felülírásának megakadályozását nevezi meg használati esetként.
Egy kicsi, de teljes végpont
Az alábbi Node-példa memóriában tárol egy dokumentumot. A revízió az ETag értéke; éles rendszerben ezt az adatbázisban kell atomikusan ellenőrizni és növelni.
import http from 'node:http';
const document = { title: 'Nyitóoldal', version: 1 };
const currentEtag = () => `"${document.version}"`;
const readJson = (request) => new Promise((resolve, reject) => {
let body = '';
request.setEncoding('utf8');
request.on('data', (chunk) => { body += chunk; });
request.on('end', () => {
try { resolve(JSON.parse(body)); } catch (error) { reject(error); }
});
});
const server = http.createServer(async (request, response) => {
if (request.url !== '/documents/42') return response.writeHead(404).end();
if (request.method === 'GET') {
response.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
ETag: currentEtag(),
});
return response.end(JSON.stringify({ title: document.title }));
}
if (request.method === 'PUT') {
if (request.headers['if-match'] !== currentEtag()) {
return response.writeHead(412, { ETag: currentEtag() }).end();
}
const { title } = await readJson(request);
document.title = title;
document.version += 1;
response.writeHead(200, { 'Content-Type': 'application/json', ETag: currentEtag() });
return response.end(JSON.stringify({ title: document.title }));
}
response.writeHead(405).end();
});
A kliens tehát nem a válasz törzséből veszi ki a verziót, hanem megőrzi a fejlécet, majd mentéskor változatlanul visszaadja:
const loaded = await fetch('/documents/42');
const etag = loaded.headers.get('etag');
const saved = await fetch('/documents/42', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'If-Match': etag,
},
body: JSON.stringify({ title: 'Új cím' }),
});
if (saved.status === 412) {
// Újratöltés, konfliktusjelzés vagy tudatos összevonás következik.
}
A 412 nem kivétel, hanem termékviselkedés
A 412-t nem érdemes automatikusan újrapróbálni ugyanazzal a törzzsel: azzal ismét a felülírást próbálnánk átvinni. Egy egyszerű adminfelület inkább töltse újra az adatot, és kérje a felhasználót a módosítás megismétlésére. Gazdag szövegszerkesztőnél vagy űrlapnál célszerű a szerver aktuális állapotát megmutatni, majd mezőnként vagy dokumentumszinten konfliktust kezelni.
Az ETag-nek itt erős validátornak kell lennie. A W/"..." előtagú gyenge ETag csak szemantikai azonosságot jelezhet; az If-Match összehasonlításához a szabvány erős összehasonlítást ír elő. Egy módosítás után tehát mindig más erős ETag-et adj. A több értéket is elfogadó If-Match lista és a * speciális érték létezik, de egy editoros API szerződésében az egyetlen, előző GET-ből kapott tag a legkönnyebben érthető forma.
Az adatbázis a kritikus pont
A fenti memóriaobjektum csak a HTTP-folyamatot mutatja. Adatbázissal hibás lenne előbb külön lekérdezni a revíziót, majd feltétel nélküli UPDATE-et küldeni: két kérés a két lépés közé csúszhat. A tényleges írás legyen egyetlen feltételes művelet, például a WHERE id = ? AND version = ? feltétellel, amely ugyanabban a statementben növeli a verziót. A nullás érintett sor jelenti a 412-t.
Az ETag-et ettől még a reprezentációra számold. Ha a végpont a cím mellett jogosultságfüggő mezőket, lokalizált szöveget vagy más nézetet is visszaad, az ETag-nek ezek változását is tükröznie kell. A feltételes HTTP-kérések a cache és az írásvédelem esetén is ugyanazt a kérdést teszik fel: ugyanarról az erőforrás-állapotról beszélünk még?
Azonos origón nincs további teendő. Külön frontend- és API-doménnél viszont a CORS válaszban az ETag-et fel kell venni az Access-Control-Expose-Headers listába, különben a böngészős JavaScript nem olvashatja ki. Az If-Match küldése pedig preflightot okozhat, ezért azt az Access-Control-Allow-Headers válaszfejlécben is engedélyezni kell.
Mikor éri meg?
Beállítások, CMS-tartalom, ügyféladatok, készlet- vagy állapotváltások és belső adminfelületek jó jelöltek. Nem oldja meg a közös, karakterenkénti dokumentumszerkesztést; oda CRDT vagy operációs transzformáció kell. Viszont az átlagos REST-erőforrásnál kevés protokollal és egy világos 412-es ággal cseréli le a néma adatvesztést kimondott konfliktusra.