Download the PHP package devaction-labs/idempotency without Composer
On this page you can find all versions of the php package devaction-labs/idempotency. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download devaction-labs/idempotency
More information about devaction-labs/idempotency
Files in devaction-labs/idempotency
Package idempotency
Short Description Elegant, production-ready idempotency middleware for Laravel APIs — smart payload hashing, per-user scoping, pluggable telemetry.
License MIT
Informations about the package idempotency
Why this exists
Your payment endpoint is a bomb waiting to go off. A mobile client's Wi-Fi hiccups, the request retries, and now you've charged the customer twice. You add a requests table with a unique constraint. A month later, a webhook consumer takes 31 seconds to respond, the sender retries, and you've just shipped two of the same order.
Idempotency is the protocol-level answer: the client sends a unique Idempotency-Key header, the server guarantees the same key produces the same outcome exactly once — even under retries, concurrency, and network failures.
This package is that guarantee, made Laravel-native.
- First request → processes and returns
201 CreatedwithIdempotency-Status: Original - Retry with same key + same payload → returns the cached
201withIdempotency-Status: Repeated - Retry with same key + different payload → returns
422(key reuse with different intent) - Concurrent retry while first is still processing → returns
409(another request in flight)
Table of contents
- Requirements
- Installation
- Quick start
- How it works
- Per-route configuration
- Configuration reference
- Scoping
- Payload fingerprinting
- Events and alerts
- Telemetry
- Custom resolvers
- Client integration
- Artisan commands
- Testing
- Deployment & hardening
- FAQ
- License
Requirements
| Dependency | Version |
|---|---|
| PHP | 8.3, 8.4, 8.5 |
| Laravel | 11.x / 12.x / 13.x (Laravel 10 reached security EOL in Aug 2025) |
| Cache store | any driver with atomic locks — redis, memcached, database, dynamodb |
Installation
The service provider auto-registers and aliases the middleware as idempotent.
Quick start
Wrap any mutating route in the idempotent middleware:
Clients opt in by sending a UUID (or ULID, or any shape you configure) in the Idempotency-Key header. Done.
How it works
- Validate the key format (UUID by default).
- Compute a scope-aware cache key (
idempotency:{scope}:{key}:response). - Acquire an atomic lock; if already held, wait up to
lock.waitseconds. - Check the cache. If present, validate the payload matches and replay.
- Otherwise, execute the route and cache the response if its status code is in range.
- Release the lock.
Every step is instrumented — see Telemetry.
Per-route configuration
Middleware parameters let you tune behaviour per route without touching config:
| Parameter | Effect |
|---|---|
optional |
Missing key is allowed — route runs without idempotency |
ttl=<seconds> |
Override the default TTL for this route |
scope=<name> |
Override the scope strategy for this route |
Configuration reference
The full file lives at config/idempotency.php. The important knobs:
Scoping
Scoping is the invisible safety net that stops user A's key from ever matching user B's cached response.
| Scope | Key partition | When to pick it |
|---|---|---|
global |
none | Internal / trusted clients only |
user |
authenticated user id | User-level idempotence across their own routes |
route |
route name or URI | Public endpoints that shouldn't cross-pollinate |
ip |
client IP | Anonymous POSTs from the same caller |
user_route (default) |
user id + route | Most apps want this |
Need something custom (tenant id, API key, device id)? Implement ScopeResolver — see Custom resolvers.
Payload fingerprinting
The package guards against "same key, different body" by hashing the payload and comparing. The hash is:
- Deterministic — keys are recursively sorted before hashing, so
{a:1,b:2}and{b:2,a:1}match. - Configurable — pick your algorithm (
sha256,xxh128, …) viapayload.algo. - File-aware — uploaded files are fingerprinted by name + size + mime + content hash.
- Redactable —
payload.ignorelets you strip volatile fields (timestamp,client_request_id) before hashing.
For public upload endpoints, set max_payload_bytes and max_file_bytes to
fail closed with 413 before the middleware spends unbounded CPU or disk I/O on
hashing. If file metadata is enough for a route, set hash_file_contents to
false and keep include_files enabled.
Response replay headers
The default serializer stores response status, body, and replay-safe headers. It
does not store sensitive or hop-by-hop headers such as Set-Cookie,
Authorization, WWW-Authenticate, Connection, or Transfer-Encoding.
Add application-specific headers to the strip list when publishing the config:
If an endpoint must replay a normally stripped header, bind a custom
ResponseSerializer for that application.
Events and alerts
The package dispatches IdempotencyAlertFired whenever something interesting happens. Listen for it and route to logs, Sentry, Slack — whatever:
Full event catalogue (DevactionLabs\Idempotency\Logging\EventType):
| Case | Fires when |
|---|---|
RESPONSE_DUPLICATE |
Hit count on a key exceeds alerts.hit_threshold |
PAYLOAD_MISMATCH |
Same key reused with a different request body |
CONCURRENT_CONFLICT |
A second request hits while the first is still processing |
LOCK_INCONSISTENCY |
Lock acquisition failed and no processing marker was found |
SIZE_WARNING |
Cached response exceeds size_warning bytes |
EXCEPTION_THROWN |
Cache or handler threw during processing |
MISSING_KEY / INVALID_KEY_FORMAT |
Client supplied a bad or absent key |
Alerts have a built-in cooldown (alerts.cooldown, defaults to 1h) so a bad client can't flood your logs. By default,
alert context redacts raw idempotency keys into idempotency_key_hash and suppresses exception messages. You can opt out
with alerts.redact_context=false or alerts.include_exception_messages=true if your logging pipeline is already scoped
for sensitive data.
Telemetry
Shipped drivers: null (default) and inspector.
The driver records: request counts, cache hit/miss, lock acquisition time, processing time, response size.
Write your own by implementing DevactionLabs\Idempotency\Contracts\TelemetryDriver and pointing telemetry.custom_driver_class at it.
Custom resolvers
Every piece of logic sits behind a contract. Swap any of them:
A tenant-aware scope, for example:
Client integration
The server is only half of the contract — the client has to do three things correctly:
- Generate one key per logical operation, not per HTTP attempt.
- Send the same key on every retry of that operation.
- Only retry on transient failures (network errors,
408,409,429,5xx).
Below are drop-in patterns for the stacks you'll actually encounter.
Key generation
Use a UUID v4/v7 — crypto.randomUUID() is in every modern browser and Node runtime:
Rule of thumb: bind the key to the user's intent, not the request. A "Pay" button click creates one key; every retry of that click reuses it. If the user clicks Pay again after seeing a final error, that's a new intent — new key.
fetch + AbortController + retry
Usage:
Axios interceptor
An interceptor that attaches an idempotency key to every mutating request, and retries on transient failures reusing the same key:
Call sites are oblivious to any of it:
React — useIdempotentMutation
A hook that guarantees one key per mount-and-submit cycle, regenerated only after success or definitive failure:
cURL / raw HTTP
For CLI tests, Postman collections, or platform docs:
Run the exact same command again — the second response will include Idempotency-Status: Repeated.
Checklist
- [x] One key per logical intent (not per click, per retry, or per render).
- [x] Key is UUID v4/v7 by default (or matches your
validation.pattern). - [x] Only retry on network errors,
408,409,425,429,5xx. - [x] Never retry on
400/401/403/422— those are definitive. - [x] Exponential backoff with jitter, cap retries at ~4.
- [x] Inspect
Idempotency-Statusheader in dev tools when debugging.
Artisan commands
The doctor command checks the hash algorithm, TTL ranges, lock TTL consistency, and whether the configured cache store can acquire atomic locks.
You can also reach the same behaviour programmatically through the facade:
Testing
The bundled Pest suite covers cache hit/miss, lock contention, payload mismatch, scope isolation, streamed-response skipping, header name override, and alert threshold firing. Run it as a living spec for how the middleware behaves.
Deployment & hardening
The middleware is safe by default, but a few production choices change the threat model. This section flags them so you don't learn about them in an incident.
1. Isolate the cache store
The response cache lives in whatever Laravel cache store you configure. If that store is shared with other applications (common with a team Redis), those apps can write keys under idempotency:* and your API will serve them.
Three options, pick one:
A cross-app key collision cannot RCE you — the serializer constructs JsonResponse/Response explicitly, never new $class() — but it can serve an attacker-controlled 200 body to your clients. Isolation is the fix.
2. Trust your proxies (if using scope=ip)
DefaultScopeResolver calls $request->ip(). Behind a reverse proxy (nginx, ALB, Cloudflare), that returns the proxy's IP unless Laravel knows to trust it.
Without this, every request from any user looks like the same IP and their keys collide.
3. Pair with RateLimiter for abuse-resistant endpoints
Idempotency prevents duplicate processing. It does not prevent key-space flooding — an attacker can still fill your cache with random keys until the TTL saves you. Combine with Laravel's rate limiter for anything public:
Order matters: throttle first keeps abusers out of idempotency storage entirely.
4. Don't run scope=global in production with auth
scope=global collapses the key namespace across all users — user A's Idempotency-Key and user B's collide. The middleware logs a warning the first time it sees an authenticated request under this scope, but do not ship it unless your API is truly single-tenant and unauthenticated.
5. Lock driver reality check
Auto-merge's atomic locks need a cache store that supports them. Quick rundown:
| Driver | Locks? | Notes |
|---|---|---|
redis |
Yes | Best. Use a dedicated DB. |
memcached |
Yes | Fine. |
database |
Yes | Works, but contention is worse under load. Use when you already have a DB and no Redis. |
dynamodb |
Yes | Fine, watch your provisioned capacity. |
array |
Yes — per process | Never across workers. Tests only. |
file |
No | Will throw at runtime. |
6. Octane / long-running workers
The middleware holds no cross-request state. The only static state is a one-shot "warned about global scope" flag in DefaultScopeResolver — that re-emits correctly after each Octane worker reload. No action needed.
7. Do not log payloads in alerts
The IdempotencyAlertFired event ships context which already excludes request bodies. If you extend it with your own listener, avoid dumping the full payload — those events go to whatever sink you configured and may contain PII or secrets.
FAQ
Do I need Redis? No, any cache store with atomic locks works (redis, memcached, database, dynamodb). The array driver is OK for tests.
What happens if the client doesn't send a key? A 400 by default. Use idempotent:optional on routes where the key is advisory.
What if my handler throws? The lock releases, the processing marker is cleared, nothing is cached, and an EXCEPTION_THROWN event fires. The retry starts fresh.
Does it cache 4xx? 400–499 are cached by default (with 408/409/425/429 excluded as transient). Adjust via cacheable_status.
Does it cache 5xx? No. Server errors are never cached — the retry runs fresh.
What about file uploads? Included in the hash by default (name + size + mime + xxh128 of contents). Disable via payload.include_files.
ULID support? 'pattern' => 'ulid'. Or supply a regex. Or a class implementing KeyValidator.
Octane-safe? Yes — no request-scoped state is held on the middleware between requests.
License
The MIT License (MIT). See LICENSE.md.
All versions of idempotency with dependencies
illuminate/contracts Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0
illuminate/events Version ^11.0|^12.0|^13.0
illuminate/http Version ^11.0|^12.0|^13.0
illuminate/cache Version ^11.0|^12.0|^13.0
illuminate/console Version ^11.0|^12.0|^13.0