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.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package idempotency

# Idempotency for Laravel **Safely retry mutating HTTP requests. No double charges. No duplicated orders. No accidental side effects.** [![Latest Version](https://img.shields.io/packagist/v/devaction-labs/idempotency.svg?style=flat-square&label=packagist)](https://packagist.org/packages/devaction-labs/idempotency) [![Tests](https://img.shields.io/github/actions/workflow/status/devaction-labs/Idempotency/tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/devaction-labs/Idempotency/actions) [![PHPStan](https://img.shields.io/badge/PHPStan-level%20max-brightgreen?style=flat-square)](phpstan.neon) [![Code Style](https://img.shields.io/badge/code%20style-pint-orange?style=flat-square)](pint.json) [![PHP](https://img.shields.io/packagist/php-v/devaction-labs/idempotency?style=flat-square)](composer.json) [![License](https://img.shields.io/github/license/devaction-labs/Idempotency?style=flat-square)](LICENSE.md)

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.


Table of contents


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

  1. Validate the key format (UUID by default).
  2. Compute a scope-aware cache key (idempotency:{scope}:{key}:response).
  3. Acquire an atomic lock; if already held, wait up to lock.wait seconds.
  4. Check the cache. If present, validate the payload matches and replay.
  5. Otherwise, execute the route and cache the response if its status code is in range.
  6. 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:

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:

  1. Generate one key per logical operation, not per HTTP attempt.
  2. Send the same key on every retry of that operation.
  3. 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


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.

Original package by @infinitypaul. v2 rewrite maintained at devaction-labs/Idempotency.

All versions of idempotency with dependencies

PHP Build Version
Package Version
Requires php Version ^8.5
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
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package devaction-labs/idempotency contains the following files

Loading the files please wait ...