php
60 lines · 8 steps
Idempotent requests with Laravel middleware
A middleware that caches successful mutating responses so retries with the same key replay the original result instead of running twice.
Explained by
highlit
1<?php
2
3namespace App\Http\Middleware;
4
5use Closure;
6use Illuminate\Http\Request;
7use Illuminate\Support\Facades\Cache;
8use Symfony\Component\HttpFoundation\Response;
9
10class IdempotentRequest
11{
12 private const TTL = 86400;
13
14 public function handle(Request $request, Closure $next): Response
15 {
16 if (! in_array($request->method(), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
17 return $next($request);
18 }
19
20 $key = $request->header('Idempotency-Key');
21
22 if (! $key) {
23 abort(400, 'Missing Idempotency-Key header.');
24 }
25
26 $cacheKey = "idempotency:{$request->user()?->id}:{$key}";
27
28 if ($cached = Cache::get($cacheKey)) {
29 return response($cached['body'], $cached['status'], $cached['headers'])
30 ->header('Idempotency-Replayed', 'true');
31 }
32
33 $lock = Cache::lock("{$cacheKey}:lock", 10);
34
35 if (! $lock->get()) {
36 abort(409, 'A request with this Idempotency-Key is already in progress.');
37 }
38
39 try {
40 if ($cached = Cache::get($cacheKey)) {
41 return response($cached['body'], $cached['status'], $cached['headers'])
42 ->header('Idempotency-Replayed', 'true');
43 }
44
45 $response = $next($request);
46
47 if ($response->isSuccessful()) {
48 Cache::put($cacheKey, [
49 'status' => $response->getStatusCode(),
50 'body' => $response->getContent(),
51 'headers' => ['Content-Type' => $response->headers->get('Content-Type')],
52 ], self::TTL);
53 }
54
55 return $response;
56 } finally {
57 $lock->release();
58 }
59 }
60}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Idempotency keys let clients safely retry mutating requests without duplicating side effects.
- 2A distributed lock plus a double-check prevents two concurrent requests with the same key from both executing.
- 3Only cache successful responses so failed attempts can be retried cleanly.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/idempotent-requests-with-laravel-middleware-explained-php-960c/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.