Idempotency-Key-Middleware
idempotent setzt At-most-once-Schreibverarbeitung
über einen vom Client gelieferten Header durch:
POST /api/paymentsIdempotency-Key: tx-1684923847-abc
→ 201 Created (erstes Mal — verarbeitet + gespeichert){ "txId": "tx-42" }
POST /api/paymentsIdempotency-Key: tx-1684923847-abc ← selber Key
→ 201 Created (zweites Mal — aus Cache zurückgegeben, keine erneute Verarbeitung){ "txId": "tx-42" }Der Handler läuft nur beim ersten Request. Spätere Requests mit demselben Key liefern die gecachte Response.
import { idempotent, path, post } from 'actor-ts/http';import { InMemoryCache } from 'actor-ts';
const dedup = idempotent({ cache: new InMemoryCache(), ttlMs: 24 * 60 * 60_000, // 24 Stunden missingHeader: 'reject', // 400, wenn der Header fehlt});
const routes = path('api', path('payments', post(dedup(processPayment))),);Warum das wichtig ist
Abschnitt betitelt „Warum das wichtig ist“Netzwerk-Retries sind häufig. Ohne Idempotenz:
Client → POST /payments (100 €) → Netzwerk-Timeout (Request war serverseitig tatsächlich erfolgreich)Client → POST /payments (100 €) ← Retry; bucht doppelt abMit Idempotency-Key sieht der Retry: “Key bereits verarbeitet, hier ist die ursprüngliche Response.” Keine Doppelbuchung.
Konfiguration
Abschnitt betitelt „Konfiguration“interface IdempotencyOptions { cache: Cache; ttlMs?: number; // Default 24 h headerName?: string; // Default 'idempotency-key' keyPrefix?: string; // Default 'idem:' missingHeader?: 'reject' | 'pass-through'; // Default 'reject'}| Feld | Zweck |
|---|---|
cache | Backing-Store. Redis ist für Multi-Pod nötig. |
ttlMs | Wie lange jeder Key gemerkt wird. Default 24 Stunden. |
headerName | Header-Namen anpassen (case-insensitive). Default 'idempotency-key'. |
keyPrefix | Cache-Key-Namespace. Default 'idem:', damit mehrere Idempotency-Wrapper im selben Redis nicht kollidieren. |
missingHeader | Was tun, wenn der Header fehlt. Default 'reject' (400); auf 'pass-through' setzen, um den Handler ohne Dedup laufen zu lassen, wenn nur manche Clients Idempotency nutzen. |
Der Wrapper speichert zusätzlich einen SHA-256-Hash des Request-Bodies neben jeder gecachten Response. Wenn ein zweiter Request mit demselben Key, aber anderem Body hereinkommt, lehnt der Wrapper mit 422 ab — das verhindert, dass ein Client (böswillig oder fehlerhaft) denselben Key für einen semantisch anderen Request wiederverwendet und so die falsche gespeicherte Response erhält.
Was gecacht wird
Abschnitt betitelt „Was gecacht wird“{ status: 201, headers: { 'content-type': 'application/json' }, body: '{"txId":"tx-42"}',}Die Middleware speichert die komplette Response. Spätere Requests mit demselben Key bekommen eine identische Response — gleicher Status, gleiche Header, gleicher Body.
Für Fehler-Responses ist das Verhalten konfigurierbar. Per Default: 4xx + 5xx werden ebenfalls gecacht (damit ein “Payment fehlgeschlagen” beim Retry nicht zu einem “Payment erfolgreich” wird). Das entspricht der Standard-Idempotency-Key- Semantik.
Scoping pro Mandant
Abschnitt betitelt „Scoping pro Mandant“Für Key-Isolation pro Mandant baue einen separaten
idempotent-Wrapper pro Mandant (oder nimm den Mandanten in
keyPrefix auf):
const dedupForTenant = (tenant: string) => idempotent({ cache, ttlMs: 24 * 60 * 60_000, keyPrefix: `idem:${tenant}:`, });Mandant As key-123 ist dann verschieden von Mandant Bs key-123.
Wichtig, wenn:
- Verschiedene Mandanten zufällig denselben Key wählen könnten.
- Du pro Mandant abrechnest oder auditest.
Multi-Pod mit Redis
Abschnitt betitelt „Multi-Pod mit Redis“import { RedisCache } from 'actor-ts';
idempotent({ cache: new RedisCache({ url: 'redis://...' }), ttlMs: 24 * 60 * 60_000,});Mit Redis-Backing sieht jeder Pod denselben Idempotenz-State — ein Retry auf pod-2, nachdem das Original pod-1 traf, liefert die gecachte Response.
InMemoryCache → State pro Pod → Retries, die andere Pods treffen, könnten doppelt verarbeiten. In Produktion immer Redis.
Wo einsetzen
Abschnitt betitelt „Wo einsetzen“POST /api/payments ✓ Idempotency-Key empfohlenPOST /api/orders ✓ dasselbePOST /api/emails ✓ Doppelversand vermeidenPUT /api/users/:id ✓ Retries sicherGET /api/users/me ✗ nicht nötig (bereits idempotent)DELETE /api/orders/:id ✓ Retries sicherAuf jeden mutierenden Endpoint, bei dem Doppelverarbeitung schädlich ist, anwenden.
Verantwortung auf Client-Seite
Abschnitt betitelt „Verantwortung auf Client-Seite“const key = `${userId}-${operation}-${Date.now()}-${random}`;
fetch('/api/payments', { method: 'POST', headers: { 'idempotency-key': key, 'content-type': 'application/json', }, body: JSON.stringify({ amount: 100 }),});
// Beim Retry: DENSELBEN KEY WIEDERVERWENDENfetch('/api/payments', { method: 'POST', headers: { 'idempotency-key': key }, // ← selber Key body: JSON.stringify({ amount: 100 }),});Der Client muss den Key generieren + mit demselben Key retrien. Wenn der Client pro Versuch einen frischen Key erzeugt, sieht die Middleware sie als verschiedene Requests und verarbeitet jeden.
Häufiger Bug: den Key innerhalb der Retry-Schleife zu erzeugen statt einmal vor dem ersten Versuch.
In-Flight-Behandlung
Abschnitt betitelt „In-Flight-Behandlung“Wenn zwei Requests mit demselben Key gleichzeitig ankommen (Doppelklick, paralleler Retry):
- Der erste, der den Cache-Lock schnappt, verarbeitet; der zweite sieht “noch in flight” und … je nach Implementierung unterschiedlich.
Die Middleware des Frameworks lockt pro Key: der zweite Request wartet, bis der erste abgeschlossen ist, und liefert dann die gecachte Response. Meist Sub-Sekunden-Wartezeit; gebunden durch die Handler-Laufzeit.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- HTTP-Übersicht — das große Bild.
- Response-Cache-Middleware — komplementäre Lese-Seite.
- Rate-Limit-Middleware — Request-Limits pro Key.
- Cache-Übersicht — der Backing-Store.