---
title: "Idempotenčné kľúče — Sentinel for Laravel | Roundly"
description: "Spracovanie Idempotency-Key pre routy, programové behy pre joby a webhooky, middleware jobov Idempotent a makro klienta."
url: https://roundly-consulting.com/sk/open-source/navody/sentinel-for-laravel/idempotencia
language: sk
---

[Všetky balíky](https://roundly-consulting.com/sk/open-source.md)

[Sentinel for Laravel](https://roundly-consulting.com/sk/open-source/navody/sentinel-for-laravel.md)

# 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.

```php
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; min\_length väčšie než max\_length sa odmietne ako neplatná konfigurácia). 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:

```php
$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');
```

```php
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.
- Výsledok, ktorý úložisko nedokáže uchovať — chýba APP\_KEY na jeho zašifrovanie, úložisko pri dokončení zlyhá —, kľúč aj tak dokončí ako neprehrateľný a chybu vyhodí znova, takže opakovanie dostane IdempotentResponseUnavailableException (409) a callback sa po uplynutí lease nespustí druhýkrát. Kľúč ostane rozpracovaný až do konca lease len pri páde medzi callbackom a dokončením.
- 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):

```php
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. |
| Job prebehol, no jeho dokončenie sa nepodarilo uložiť | Kľúč sa dokončí ako neprehrateľný a chyba sa znovu vyhodí; opätovné doručenie job už nikdy nespustí. |
| Opätovné doručenie kľúča dokončeného ako neprehrateľný | Zlyhá s IdempotentResponseUnavailableException (409, idempotent\_response\_unavailable) bez spustenia jobu — zámerne sa nepreskočí (pozri nižšie). |

## Job, ktorý opakovane zlyháva s 409

Kľúč sa dokončí ako neprehrateľný, keď jeho beh svoju prácu urobil, no Sentinel nedokázal zaznamenať čisté dokončenie. Vedľajší účinok — platba, e-mail, volanie API — teda pravdepodobne nastal, a preto znova doručený job s týmto kľúčom (opakovanie neúspešného pokusu frontou, druhé odoslanie) zlyhá s IdempotentResponseUnavailableException (409, rejection() = IdempotencyRejection::Unavailable) namiesto toho, aby sa potichu dokončil ako bežný duplikát. Je to zámer: preskočenie by zakrylo beh, ktorého výsledok nikto nezaznamenal, a nové spustenie by mohlo vedľajší účinok zopakovať. Fronta job zopakuje až do jeho $tries — každý pokus zlyhá bez spustenia — a potom ho presunie do failed\_jobs; každé ďalšie doručenie zlyhá rovnako, kým kľúč neexpiruje. Čo urobiť:

- Preskúmajte: nájdite chybu prvého pokusu (vo vašich logoch alebo vo failed\_jobs — chybu úložiska alebo IdempotentResultException s jej príčinou) a overte, či vedľajší účinok nastal (u poskytovateľa platieb, v logu e-mailov, v API partnera).
- Nastal: nič viac netreba — job zmažte (php artisan queue:forget <id>, hromadne queue:prune-failed). Kľúč ostane dokončený, kým neexpiruje, a duplikáty ďalej odmieta.
- Nenastal alebo sa má spustiť znova: uvoľnite kľúč cez Sentinel::idempotency()->forget($key, $scope) — s tým istým kľúčom a scope, s ktorými bol middleware vytvorený — a potom spustite php artisan queue:retry <id>. Ďalší pokus job spustí.

Ak sa to chcete dozvedieť hneď a nie až dodatočne, zachyťte výnimku v metóde failed() jobu:

```php
use RoundlyConsulting\Sentinel\Exceptions\IdempotentResponseUnavailableException;

public function failed(Throwable $exception): void
{
    if ($exception instanceof IdempotentResponseUnavailableException) {
        // The key's first run may have charged already: page someone to inspect it.
    }
}
```

## Strana klienta

```php
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 kryptomien](https://roundly-consulting.com/sk/podporte-nas.md)

Odoslaním daru súhlasíte s našimi [podmienkami prijímania darov](https://roundly-consulting.com/sk/podmienky-darov.md).

[Podporte našu open-source prácu (otvorí sa v novej karte)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Pridajte sa k nám na Patreone (otvorí sa v novej karte)](https://www.patreon.com/cw/roundly)

## 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.

[Ponuka do 48 hodín](https://roundly-consulting.com/sk/kontakt.md) [Prezrieť všetky balíky](https://roundly-consulting.com/sk/open-source/navody/sentinel-for-laravel.md)
