feat: add middleware
This commit is contained in:
parent
863f3fffc5
commit
dfaec2d948
104
app/Http/Middleware/IdempotencyMiddleware.php
Normal file
104
app/Http/Middleware/IdempotencyMiddleware.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?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
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user