header('X-Idempotency-Key'); if (! is_string($idempotencyKey) || ! Str::isUuid($idempotencyKey)) { return response()->json([ 'message' => 'X-Idempotency-Key MUST be a valid UUID.', ], Response::HTTP_BAD_REQUEST); } $cacheKey = $this->cacheKey($request, $idempotencyKey); $lockKey = $this->lockKey($request, $idempotencyKey); $lock = Cache::lock($lockKey, config('slots.idempotency_lock_seconds')); if (! $lock->get()) { return $this->waitForCachedResponse($cacheKey) ?? $this->inProgressResponse(); } $response = $next($request); try { $cached = Cache::get($cacheKey); if (is_array($cached)) { return $this->replayResponse($cached); } if ($response instanceof JsonResponse) { Cache::put($cacheKey, [ 'status' => $response->getStatusCode(), 'body' => $response->getData(true), ], config('slots.idempotency_cache_ttl_seconds')); } return $response; } finally { $lock->release(); } } private function waitForCachedResponse(string $cacheKey): ?JsonResponse { $deadline = microtime(true) + config('slots.idempotency_wait_seconds'); while (microtime(true) < $deadline) { $cached = Cache::get($cacheKey); if (is_array($cached) && isset($cached['body'], $cached['status'])) { return $this->replayResponse($cached); } usleep(100_000); } return null; } private function inProgressResponse(): ?JsonResponse { return response()->json([ 'message' => 'Conflict. Retry with same idempotency key', ], Response::HTTP_CONFLICT) ->header('X-Retry-After', 1); } /** * @param array{status: int, body: string} $data */ private function replayResponse(array $data): JsonResponse { return response()->json($data['body'], (int) $data['status']); } private function cacheKey(Request $request, string $idempotencyKey): string { return sprintf( 'idempotency:response:%s:%s:%s', strtoupper($request->method()), $request->path(), $idempotencyKey ); } private function lockKey(Request $request, string $idempotencyKey): string { return sprintf( 'idempotency:lock:%s:%s:%s', strtoupper($request->method()), $request->path(), $idempotencyKey ); } }