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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Idempotency keys let clients safely retry mutating requests without duplicating side effects.
  2. 2A distributed lock plus a double-check prevents two concurrent requests with the same key from both executing.
  3. 3Only cache successful responses so failed attempts can be retried cleanly.

Related explainers

Share this explainer

Here's the card — post it anywhere.

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