php 61 lines · 8 steps

Idempotency keys in Laravel middleware

Middleware that caches write responses by Idempotency-Key so retried requests 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 IdempotencyKey
11{
12 private const TTL = 86400;
13 private const LOCK_TTL = 30;
14 
15 public function handle(Request $request, Closure $next): Response
16 {
17 if (! in_array($request->method(), ['POST', 'PUT', 'PATCH'])) {
18 return $next($request);
19 }
20 
21 $key = $request->header('Idempotency-Key');
22 
23 if (! $key) {
24 return response()->json([
25 'message' => 'Missing Idempotency-Key header.',
26 ], 400);
27 }
28 
29 $cacheKey = "idempotency:{$request->user()->id}:{$key}";
30 
31 if ($stored = Cache::get($cacheKey)) {
32 return response($stored['body'], $stored['status'])
33 ->withHeaders($stored['headers'])
34 ->header('Idempotent-Replayed', 'true');
35 }
36 
37 $lock = Cache::lock("{$cacheKey}:lock", self::LOCK_TTL);
38 
39 if (! $lock->get()) {
40 return response()->json([
41 'message' => 'A request with this Idempotency-Key is already being processed.',
42 ], 409);
43 }
44 
45 try {
46 $response = $next($request);
47 
48 if ($response->getStatusCode() < 500) {
49 Cache::put($cacheKey, [
50 'status' => $response->getStatusCode(),
51 'body' => $response->getContent(),
52 'headers' => ['Content-Type' => $response->headers->get('Content-Type')],
53 ], self::TTL);
54 }
55 
56 return $response;
57 } finally {
58 $lock->release();
59 }
60 }
61}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Idempotency keys let clients safely retry writes by replaying the stored response instead of re-executing the action.
  2. 2A short-lived lock prevents two concurrent requests with the same key from both running before either result is cached.
  3. 3Scoping the cache key to the user and skipping 5xx responses keeps replays private and lets genuine failures be retried.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotency keys in Laravel middleware — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code