amrachraf6699 / laravel-idempotency-ledger
Conservative, database-backed HTTP idempotency for Laravel.
Package info
github.com/amrachraf6699/laravel-idempotency-ledger
pkg:composer/amrachraf6699/laravel-idempotency-ledger
Requires
- php: ^8.2
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^8.37|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0
README
Conservative, database-backed HTTP idempotency for Laravel 10–13 APIs. It prevents duplicate execution for a scoped Idempotency-Key and replays supported completed responses.
Installation
composer require amrachraf6699/laravel-idempotency-ledger php artisan vendor:publish --tag=idempotency-config php artisan vendor:publish --tag=idempotency-migrations php artisan migrate
Usage
Apply the idempotent middleware after authentication:
Route::post('/payments', StorePaymentController::class) ->middleware(['auth:sanctum', 'idempotent']);
Clients send one random key per logical operation:
Idempotency-Key: 0f8fad5b-d9cb-469f-a165-70867728950e
The package scopes keys to the authenticated identity by default. Public and multi-tenant APIs must provide a ScopeResolver in config/idempotency.php.
Behavior
- Supported buffered 2xx–4xx responses are stored for 24 hours and replayed with
Idempotency-Replayed: true. - Reusing a key with a changed method, route, content type, or body returns
422. - A concurrent request returns
409andRetry-After: 1. - 5xx responses and unexpected exceptions become
indeterminate; the package will not execute them again automatically. - Throw
RetryableIdempotencyExceptiononly when no irreversible side effect began. It returns503, and the next identical request may claim a new attempt. - Streams, downloads, binary responses, oversized bodies, and undecryptable stored bodies are completed but non-replayable, so duplicate execution remains blocked.
Response bodies are encrypted using Laravel's encrypter. Set a dedicated, stable IDEMPOTENCY_HASH_KEY in production. Do not rotate that key while ledger records may still be queried.
Cleanup
Schedule idempotency:prune. It removes expired completed records and marks overdue processing claims as indeterminate; it never retries or removes indeterminate records.
Schedule::command('idempotency:prune')->daily();
Development
composer test
composer format
composer analyse