method(), ['POST', 'PUT', 'PATCH', 'DELETE'])) { return $next($request); } $idempotencyKey = $request->header('Idempotency-Key') ?? $request->header('X-Idempotency-Key'); if (!$idempotencyKey) { return $next($request); } // Clean key $key = 'idempotency:' . hash('sha256', $idempotencyKey); $lockKey = $key . ':lock'; $ttl = config('wasl.security.idempotency.ttl_seconds', 86400); // 24 hours default // Acquire lock using Redis SETNX (Swoole safe) $lockAcquired = Redis::set($lockKey, 'locked', 'EX', 10, 'NX'); if (!$lockAcquired) { return response()->json([ 'error' => 'Conflict', 'message' => 'A request with this Idempotency-Key is already in progress.', ], Response::HTTP_CONFLICT); } try { // Check if we have a cached response $cached = Redis::get($key); if ($cached) { $data = json_decode($cached, true); // Release lock Redis::del($lockKey); return response($data['content'], $data['status'], $data['headers']); } // Execute request $response = $next($request); // Only cache successful or non-server-error responses (2xx and 4xx, exclude 5xx) if ($response->getStatusCode() < 500) { $cacheData = json_encode([ 'status' => $response->getStatusCode(), 'headers' => collect($response->headers->all())->map(fn($val) => $val[0])->toArray(), 'content' => $response->getContent(), ]); Redis::set($key, $cacheData, 'EX', $ttl); } return $response; } finally { // Always release lock Redis::del($lockKey); } } }