Idempotenčné kľúče
Klienti posielajú hlavičku Idempotency-Key (draft-ietf-httpapi-idempotency-key-header-07). Prvá požiadavka prebehne; opakovania dostanú uloženú odpoveď s Idempotent-Replayed: true; ten istý kľúč s iným obsahom odpovie 422 a požiadavka, ktorá ešte beží, 409 s Retry-After. Odpovede 5xx kľúč uvoľnia, aby to klient mohol skúsiť znova.
Route::post('/orders', StoreOrder::class)->middleware(['auth:sanctum', 'sentinel.idempotent']); // optional key
Route::post('/payments', StorePayment::class)->middleware(['auth:sanctum', 'sentinel.idempotent:required']);
Route::post('/exports', StoreExport::class)->middleware('sentinel.idempotent:optional,3600'); // own TTL
// The same, validated when the route is declared (a TTL outside 60–2 592 000 throws):
Route::post('/payments', StorePayment::class)->middleware(['auth:sanctum', EnsureIdempotency::required()]);
Route::post('/exports', StoreExport::class)->middleware(EnsureIdempotency::optional(ttl: 3600));Zaraďte ho za auth, aby bol kľúč viazaný na používateľa.
Poradie kontrol
- Metódy mimo idempotency.methods (POST, PATCH) prejdú bez zmeny.
- Hlavička sa parsuje ako reťazec RFC 9651; s accept_unquoted sa prijme aj hodnota bez úvodzoviek. Dĺžka musí byť min_length..max_length (16..255). Neplatná alebo opakovaná → 400 invalid_idempotency_key; chýbajúca → 400 idempotency_key_missing pri :required, inak prejde.
- Scope: user:<guard>:<id> pre prihláseného, inak sig:<ring>:<keyid>, ak je na požiadavke overený HTTP podpis, inak ip:<ip> — plus routa. Uložený kľúč je digest zo scope, routy a kľúča.
- Odtlačok: metóda, surová cesta, surový query reťazec, content type a SHA-256 surového tela. Pri multipart/form-data sa namiesto toho viažu polia v poradí a pri každom súbore jeho názov od klienta, veľkosť a SHA-256.
| Situácia | Odpoveď |
|---|---|
| Žiadny záznam, alebo expirovaný (po TTL a bez živého lease) | Prevezme ho (processing) → spustí handler |
| Iný odtlačok (v akomkoľvek stave) | 422 idempotency_key_reused |
| processing, lease platný | 409 idempotency_request_in_progress + Retry-After |
| processing, lease vypršal (lock_seconds) | Prevezme ho (compare-and-swap na tokene vlastníka) → spustí handler |
| completed, dá sa prehrať | Prehrá uloženú odpoveď + Idempotent-Replayed: true |
| completed, nedá sa prehrať (streamovaná, príliš veľká) | 409 idempotent_response_unavailable |
| Uložený záznam bol upravený (nečitateľný) | 409 idempotent_response_unavailable — nikdy sa nespustí dvakrát naslepo |
Čo sa ukladá
- Status < 500: status, hlavičky z replayed_headers a telo — pri zapnutom idempotency.encrypt (predvolené) zašifrované AES-256-GCM pod kľúčom odvodeným z APP_KEY len pre uložené odpovede, viazané na digest a scope kľúča, takže odpoveď skopírovaná k inému kľúču alebo podstrčená ako nešifrovaný text sa nikdy neprehrá. 4xx len so store_client_errors (predvolene zapnuté).
- ≥ 500: kľúč sa uvoľní, aby to klient mohol skúsiť znova (pokiaľ nie je zapnuté store_server_errors).
- Streamované či súborové odpovede a telá nad max_response_bytes sa dokončia, ale nedajú sa prehrať. Set-Cookie sa nikdy neukladá ani neprehráva.
- Kľúče expirujú ttl sekúnd (predvolene 24 hodín) od prvého výskytu — uveďte to v dokumentácii svojho API — no nikdy, kým ich drží živý lease.
Odmietnutia sú odpovede application/problem+json podľa RFC 9457 s členom code a spúšťajú IdempotencyRejected; prehratia spúšťajú IdempotentRequestReplayed. So zapnutým idempotency.transactional (len úložisko database) sa handler a idempotenčný záznam potvrdia v jednej transakcii — práve raz pre zápisy na tomto pripojení, za cenu transakcie držanej počas celej požiadavky.
Programové behy
Pre joby, príkazy a spracovanie webhookov:
$result = Sentinel::idempotency()->run(
"charge:{$order->id}",
scope: 'billing',
callback: fn () => $gateway->charge($order),
fingerprint: "order:{$order->id}:{$order->total}", // optional — reuse with another one → 422
ttl: 3600, // optional
lease: 900, // optional — above the callback's longest run
);
$result->value; // the JSON round-trip of the callback's result — the same on every run
$result->replayed; // true on repeats
$result->firstSeenAt;
Sentinel::idempotency()->forget("charge:{$order->id}", scope: 'billing');use RoundlyConsulting\Sentinel\Actions\Idempotency\RunIdempotentAction;
use RoundlyConsulting\Sentinel\DataTransferObjects\IdempotentCall;
$result = Sentinel::runIdempotent(new IdempotentCall("charge:{$order->id}", 'billing', fn () => $gateway->charge($order)));
Sentinel::forgetIdempotencyKey("charge:{$order->id}", 'billing'); // bool: whether it existed
$result = app(RunIdempotentAction::class)->execute(new IdempotentCall("charge:{$order->id}", 'billing', fn () => $gateway->charge($order)));- Kľúč aj scope majú 1–255 bajtov — scope nikdy nie je prázdny — ttl je 60–2 592 000 sekúnd a lease 1–86 400 sekúnd; čokoľvek iné vyhodí InvalidIdempotencyKeyException skôr, než sa siahne na úložisko. Zvoľte si vlastné scopes (billing, webhooks), nie také, ktoré pripomínajú HTTP scopes.
- fingerprint odmietne opätovné použitie kľúča s iným vstupom (IdempotencyKeyReusedException).
- value je vždy to, čo vráti prehratie — JSON round-trip výsledku callbacku — takže $result->value['id'] funguje pri prvom behu aj pri každom opakovaní.
- Callback, ktorý vyhodí výnimku, kľúč vráti. Callback, ktorý prebehol, no vrátil niečo, čo sa nedá uložiť (binárne dáta, INF, resource), sa už nikdy nespustí: jeho kľúč sa dokončí bez výsledku (IdempotentResultException) a opakovanie sa odmietne (IdempotentResponseUnavailableException, 409), aby sa vedľajší účinok nezopakoval.
- Lease určuje, ako dlho bežiace volanie drží kľúč; callbacku, ktorý môže bežať dlhšie než lock_seconds, dajte lease dlhší než jeho najdlhší beh.
Joby vo fronte
Middleware jobov Idempotent spustí job najviac raz pre kľúč — webhook doručený znova ako druhý job, dvojité odoslanie — teda nad rámec toho, čo pokrýva ShouldBeUnique (len kým je job vo fronte) a WithoutOverlapping (len súbežne):
use RoundlyConsulting\Sentinel\Jobs\Middleware\Idempotent;
final class HandleStripeEvent implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public function __construct(public readonly StripeEvent $event) {}
public function middleware(): array
{
return [new Idempotent("stripe:{$this->event->id}", scope: 'webhooks')];
}
}new Idempotent(string $key, string $scope = 'jobs', ?int $ttl = null, int $releaseAfter = 10, ?int $lease = null) overí argumenty už pri vytvorení. Bežiaci job drží kľúč počas svojho lease: lease, ak je zadaný, inak $timeout jobu, inak retry_after pripojenia fronty — nikdy menej než idempotency.lock_seconds a najviac deň. Fronta sync nemá retry_after, takže job bez $timeout tam drží kľúč len lock_seconds (predvolene 60 sekúnd) a duplikát, ktorý príde neskôr, sa spustí popri prvom behu, ktorý ešte neskončil: takému jobu dajte $timeout alebo zadajte lease.
| Situácia | Výsledok |
|---|---|
| Prvý beh sa dokončí | Kľúč sa dokončí (výsledok null). |
| Duplikát potom | Dokončí sa bez spustenia. |
| Duplikát, kým prvý ešte beží | release(max(Retry-After, releaseAfter)) späť do fronty — alebo znovu vyhodí IdempotencyRequestInProgressException pri jobe bez release(). |
| Job vyhodí výnimku | Kľúč sa uvoľní; fronta job zopakuje alebo označí za neúspešný ako zvyčajne. |
| Job sám zavolá release() alebo fail() | Kľúč sa uvoľní (beh sa nedokončil), takže opakovanie prebehne. |
Strana klienta
Http::withIdempotencyKey()->post('https://api.example.com/orders', $payload); // a fresh UUIDv7
Http::withIdempotencyKey($order->uuid)->post('https://api.example.com/orders', $payload);Úložiská
database (predvolené) používa sentinel_idempotency_keys s jedinečným digestom kľúča, insert-or-ignore a compare-and-swap na tokene vlastníka. cache vyžaduje store s atomickými zámkami (inak sa pri štarte odmietne). Pre vlastné úložisko naviažte Contracts\IdempotencyStore (pozri Rozšírenie). Všetky úložiská zdieľajú jednu rozhodovaciu tabuľku a osem súbežných požiadaviek s rovnakým kľúčom na PostgreSQL aj MySQL má práve jedného vlastníka.
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.