NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky
Sentinel for Laravel

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áciaOdpoveď
Ž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áciaVýsledok
Prvý beh sa dokončíKľúč sa dokončí (výsledok null).
Duplikát potomDokončí 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ýnimkuKľúč 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 kryptomien

Odoslaní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.