stl_test/app/Http/Middleware/IdempotencyMiddleware.php
2026-05-17 19:50:49 +07:00

105 lines
3.0 KiB
PHP

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class IdempotencyMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$idempotencyKey = $request->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
);
}
}