HTTP-Vorbedingungen für sichere Updates - Veraltete Editoren sollen scheitern, statt neue Arbeit zu überschreiben
Zwei Personen öffnen denselben Artikel in einem Editor. Beide sehen Revision 17. Eine Person korrigiert die Einleitung und speichert Revision 18. Die andere Person, die weiterhin Revision 17 vor sich hat, ändert den Schluss und speichert einige Minuten später. Wenn die zweite Anfrage den gespeicherten Inhalt einfach ersetzt, verschwindet die Arbeit der ersten Person, ohne dass einer der beiden Editoren einen Fehler anzeigt.
Das ist das Problem des Lost Update. Dafür braucht es kein großes verteiltes System; zwei Browser-Tabs, ein API-Client und eine Administrationsseite oder eine verzögerte mobile Anfrage genügen. Eine Datenbanktransaktion kann einen einzelnen Schreibvorgang intern konsistent halten, teilt dem Server aber nicht von selbst mit, ob der Client mit aktuellen Daten begonnen hat. HTTP besitzt bereits ein Vokabular für diese fehlende Frage: Validatoren und Vorbedingungen.
Der Client muss angeben, welche Version er bearbeitet hat
Eine Anfrage wie PUT /api/posts/42 benennt die zu ändernde Ressource und enthält die vorgeschlagene neue Repräsentation. Ohne eine weitere Bedingung kann sie zugleich bedeuten: „Wende dies unabhängig davon an, was seit meinem letzten Lesen geschehen ist.“ Für manche Operationen mag das richtig sein, bei einem Editor, der eine Repräsentation ersetzt, ist es jedoch riskant.
RFC 9110 Abschnitt 13 definiert eine bedingte Anfrage als HTTP-Anfrage, deren Header-Felder eine Vorbedingung angeben, die vor der Anwendung der Methode geprüft wird. Für eine Aktualisierung lautet die nützliche Vorbedingung häufig:
Wende diese Änderung nur an, wenn die Ressource noch dieselbe Version besitzt, die der Client zuvor erhalten hat.
Der Server übermittelt diese Version mit einem Validator. HTTP definiert Änderungsdaten und Entity-Tags als gebräuchliche Formen von Validatoren. Ein ETag ist ein opaker, vom Origin-Server gewählter Wert; er muss weder ein Content-Hash noch ein Datenbank-Zeitstempel oder eine öffentliche Revisionsnummer sein. Entscheidend ist, ob er sich zusammen mit der von ihm validierten Repräsentation ändert.
Ein einfacher Austausch mit GET und PUT
Nehmen wir an, der Client ruft zunächst einen Artikel ab:
GET /api/posts/42 HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "post-17"
{"title":"A careful title","content":"..."}
Der Client sollte das Entity-Tag gemeinsam mit den angezeigten Daten aufbewahren. Beim Speichern sendet er genau diesen Validator in If-Match zurück:
PUT /api/posts/42 HTTP/1.1
Host: example.com
Content-Type: application/json
If-Match: "post-17"
{"title":"A more careful title","content":"..."}
Ist Revision 17 weiterhin aktuell, kann der Server den Schreibvorgang anwenden und einen Validator für die neue Repräsentation zurückgeben. Hat eine andere Anfrage bereits Revision 18 erzeugt, ist die Bedingung falsch. Der Server darf die angeforderte Methode nicht einfach so anwenden, als wäre die Bedingung nicht vorhanden. Die übliche Antwort für diesen Ablauf lautet:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
{"error":"The post changed after you loaded it. Fetch the current version before saving."}
RFC 9110 definiert 412 für Bedingungen in einer Anfrage, die als falsch ausgewertet werden. Dieser Fehler ist eine nützliche Information und nicht nur ein Hindernis: Beide Versionen bleiben lange genug erhalten, damit die Anwendung ein erneutes Laden, einen Vergleich oder eine bewusste Zusammenführung anbieten kann. Den veralteten Inhalt automatisch erneut zu senden, ohne den Unterschied zuvor zu klären, würde genau das Überschreiben wiederholen, das die Vorbedingung verhindert hat.
If-Match benötigt einen starken Validator
Die Einzelheiten des Tags sind wichtig. Nach den If-Match-Regeln in RFC 9110 werden Entity-Tags mit der starken Vergleichsfunktion verglichen. Ein schwaches Tag wie W/"post-17" kann Repräsentationen beschreiben, die der Server für bestimmte Zwecke als gleichwertig betrachtet, es kann If-Match jedoch nicht erfüllen. Die MDN-Referenz fasst die praktische Folge zusammen: Ein schwaches Entity-Tag stimmt in diesem Header niemals überein.
Ein Revisionszähler kann nur dann ein geeignetes starkes Tag liefern, wenn seine Invariante stark genug ist: Jede Änderung, die in der ausgewählten Repräsentation sichtbar ist, muss zu einem anderen Validator führen. Enthält das JSON eines Artikels Titel, Inhalt und Veröffentlichungsstatus, während der Zähler nur bei Änderungen des Inhaltsfeldes steigt, identifiziert das Tag diese Repräsentation nicht zuverlässig. Ein kollisionsresistenter Hash der endgültigen Repräsentation ist eine weitere Möglichkeit, doch seine Berechnung kann unnötig sein, wenn die Anwendung bereits über eine verlässliche Revisionskontrolle verfügt.
Zeitangaben sind weniger geeignet, wenn eine genaue Änderungserkennung erforderlich ist. RFC 9110 weist darauf hin, dass die Auflösung einer Uhr eine Änderungszeit zu einem schwachen Validator machen kann, wenn sich eine Ressource innerhalb dieser Auflösung mehrmals ändern kann. If-Unmodified-Since steht für Server zur Verfügung, die keine Entity-Tags liefern, doch ein klar definierter starker ETag ist bei einem von der Anwendung kontrollierten Editor meist leichter nachzuvollziehen.
Auch die Prüfung im Speicher muss atomar sein
Korrekte HTTP-Felder beheben kein Check-then-write-Rennen innerhalb der Anwendung. Stellen wir uns vor, PHP liest version = 17, vergleicht den Header und führt anschließend ein unbedingtes UPDATE aus. Zwischen dem Lesen und dem Update könnte eine andere Anfrage Revision 18 committen. Beide HTTP-Anfragen scheinen korrekt geprüft worden zu sein, dennoch kann die spätere SQL-Anweisung neuere Arbeit überschreiben.
Ein kompakter Ansatz besteht darin, die erwartete Version in den Schreibvorgang selbst aufzunehmen. Dieses illustrative PDO-Fragment setzt voraus, dass Authentifizierung, Autorisierung, JSON-Validierung, die Prüfung auf das Vorhandensein der Ressource und die Ausnahmebehandlung bereits erfolgt sind. Es akzeptiert bewusst nur einen einzelnen, von der Anwendung erzeugten ETag, statt die vollständige von HTTP erlaubte Listensyntax zu implementieren:
<?php
$ifMatch = $_SERVER['HTTP_IF_MATCH'] ?? null;
if ($ifMatch === null) {
http_response_code(428);
echo json_encode(['error' => 'Send If-Match with the current post ETag.']);
exit;
}
if (!preg_match('/^"post-(\d+)"$/D', $ifMatch, $match)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid If-Match value.']);
exit;
}
$expectedVersion = (int) $match[1];
$update = $pdo->prepare(
'UPDATE posts
SET title = :title,
content = :content,
version = version + 1
WHERE id = :id AND version = :expected_version'
);
$update->execute([
'title' => $validatedTitle,
'content' => $validatedContent,
'id' => $postId,
'expected_version' => $expectedVersion,
]);
if ($update->rowCount() !== 1) {
http_response_code(412);
echo json_encode(['error' => 'The post changed; fetch the current version.']);
exit;
}
$newVersion = $expectedVersion + 1;
header('ETag: "post-' . $newVersion . '"');
http_response_code(204);
Entscheidend ist das bedingte UPDATE. Nur eine Zeile, die weiterhin die erwartete Version besitzt, kann geändert werden, und die Version wird in derselben Anweisung erhöht. Null betroffene Zeilen bedeuten, dass dieser Schreibvorgang nicht gewonnen hat. Ein realer Endpunkt muss eine fehlende oder nicht zugängliche Ressource gemäß seiner API-Richtlinie unterscheiden, bevor dieses Fragment erreicht wird.
Bei einer Änderung über mehrere Tabellen gehören alle erforderlichen Schreibvorgänge in eine Datenbanktransaktion, wobei die Versionsprüfung Teil dieser Transaktion sein muss. HTTP liefert die Vorbedingung des Clients; die Datenbank muss weiterhin die Speicherinvariante schützen. Beide Ebenen arbeiten zusammen und sind keine konkurrierenden Alternativen.
412, 428 und 409 beantworten unterschiedliche Fragen
Diese drei Statuscodes lassen sich leicht verwechseln:
- 412 Precondition Failed: Der Client hat eine erkannte Vorbedingung gesendet, sie wurde gegenüber dem aktuellen Zustand der Ressource jedoch als falsch ausgewertet.
- 428 Precondition Required: Der Origin verlangt eine bedingte Anfrage, aber die erforderliche Bedingung fehlt.
- 409 Conflict: Die Anfrage steht in einem allgemeineren Konflikt mit dem aktuellen Zustand der Ressource, der nicht genauer als gescheiterte HTTP-Vorbedingung ausgedrückt wird.
RFC 6585 definiert 428 ausdrücklich dafür, dass ein Origin bedingte Anfragen verlangen kann; die Vermeidung verlorener Aktualisierungen ist das typische Beispiel. Der Status ist optional, daher muss eine API dokumentieren, ob sie diesen Vertrag durchsetzt. Ein Server, der eine unbedingte Aktualisierung stillschweigend akzeptiert, ist nicht allein deshalb geschützt, weil seine GET-Antworten ETags enthalten.
409 bleibt für Konflikte in der Fachlogik nützlich: Vielleicht passt ein angeforderter Zustandsübergang nicht zum aktuellen Workflow-Zustand der Ressource. Wenn eine tatsächliche If-Match-Bedingung fehlschlägt, beschreibt 412 das Protokollereignis genauer.
Nur erstellen, wenn noch nichts vorhanden ist
Die verwandte Bedingung If-None-Match: * drückt eine weitere nützliche Absicht aus: Wende eine unsichere Methode nur an, wenn das Ziel keine aktuelle Repräsentation besitzt. Damit wird aus „Erstelle diesen Namen“ die Anweisung „Erstelle diesen Namen, aber ersetze nichts, was bereits vorhanden ist.“ RFC 9110 beschreibt dies ausdrücklich als Schutz für den Fall, dass mehrere Clients versuchen könnten, die erste Repräsentation zu erstellen.
PUT /api/pages/about HTTP/1.1
Content-Type: application/json
If-None-Match: *
{"title":"About","content":"..."}
Das ist nicht nur theoretisches Protokollzubehör. Amazon S3 dokumentiert bedingte Schreibvorgänge sowohl mit If-Match als auch mit If-None-Match. Berechtigungen, Objektversionierung und Fehlerdetails sind produktspezifisch, doch das Beispiel zeigt dasselbe HTTP-Vokabular bei realen Schreiboperationen.
Was Vorbedingungen nicht lösen
Ein übereinstimmender Validator besagt, dass sich die betreffende Repräsentation nach der Validator-Richtlinie des Servers nicht geändert hat. Er besagt nicht, dass der Benutzer sie bearbeiten darf, dass die übermittelten Felder gültig sind oder dass die Anfrage gegen Cross-Site Request Forgery geschützt ist. Autorisierung, Eingabevalidierung und Sicherheitskontrollen im Browser bleiben erforderlich.
Vorbedingungen erkennen außerdem einen veralteten Schreibvorgang; sie führen ihn nicht zusammen. Ein Texteditor könnte beide Revisionen anzeigen und den Benutzer bitten, sie abzugleichen. Ein Einstellungsformular könnte neu laden und verlangen, dass Änderungen erneut eingegeben werden. Kollaboratives Bearbeiten auf Zeichenebene benötigt ein anderes Modell. Die richtige Wiederherstellung hängt davon ab, was sich sicher zusammenführen lässt.
Schließlich muss eine ETag-Richtlinie die Auswahl der Repräsentation berücksichtigen. Wenn dieselbe Ressource wesentlich unterschiedliche JSON-, HTML-, komprimierte oder sprachabhängige Repräsentationen besitzt, benötigt der Server Validatoren, die die ausgewählte Repräsentation korrekt identifizieren. Ein ansprechend aussehendes Tag, das eine sichtbare Änderung gelegentlich überlebt, ist schlechter als eine explizite, getestete Revisionsregel.
Ein gezielter Testplan
- Eine Ressource per GET abrufen und prüfen, ob die Antwort einen starken ETag enthält.
- Die Ressource mit dem passenden Wert in
If-Matchaktualisieren; Erfolg und einen neuen Validator bestätigen. - Die alte Anfrage mit dem veralteten Tag wiederholen; 412 bestätigen und prüfen, dass sich der gespeicherte Inhalt nicht geändert hat.
If-Matchbei einem Endpunkt weglassen, der es verlangt; die dokumentierte Antwort 428 bestätigen.- Zwei Schreibvorgänge mit demselben Validator gleichzeitig ausführen; bestätigen, dass höchstens ein bedingtes Speicher-Update erfolgreich ist.
- Jedes in der Antwort repräsentierte Feld ändern und prüfen, dass jede sichtbare Änderung einen anderen starken Validator erzeugt.
- Autorisierungs- und Validierungsfehler getrennt testen, damit die Behandlung von Vorbedingungen keinen Zustand unzugänglicher Ressourcen preisgibt.
Fazit
Verlorene Aktualisierungen lassen sich weder durch einen schnelleren Speichern-Button noch dadurch lösen, dass ein einzelner unbedingter Schreibvorgang in eine Transaktion eingeschlossen wird. Der Server muss wissen, welchen Zustand der Client bearbeitet hat, und diese Aussage muss mit dem Schreibvorgang im Speicher verbunden bleiben.
Ein starker ETag, If-Match, eine atomare Versionsprüfung und eine klare 412-Antwort bilden einen kleinen, aber bedeutsamen Vertrag: Veraltete Arbeit scheitert sichtbar, statt unbemerkt neuere Arbeit zu löschen. Diesen Vertrag mit 428 vorzuschreiben, kann den unbedingten Pfad schließen. Was nach einem Konflikt geschieht, bleibt eine Entscheidung der Anwendung; den Konflikt zunächst zu erhalten, ist jedoch der erste Schritt zu einer ehrlichen Lösung.
References
- RFC 9110: HTTP Semantics - IETF / RFC Editor, June 2022.
- RFC 6585 Section 3: 428 Precondition Required - IETF / RFC Editor, April 2012.
- If-Match header - MDN Web Docs, last modified July 4, 2025.
- How to prevent object overwrites with conditional writes - Amazon Web Services documentation.
