Skip to content

Instantly share code, notes, and snippets.

@OSDDQD
Created January 19, 2023 00:50
Show Gist options
  • Save OSDDQD/0b0472a926ca15b9b2d01eabd4662232 to your computer and use it in GitHub Desktop.
Save OSDDQD/0b0472a926ca15b9b2d01eabd4662232 to your computer and use it in GitHub Desktop.
Prevent Concurrent Requests
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Cache;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
class ThrottleMiddleware
{
/**
* Amount of time (in seconds) the last request will be stored,
* in the cache, in the case the middleware never terminates.
*
* @var int
*/
protected $cacheForSeconds = 3600;
/**
* The limit of concurrent requests the current user can run.
*
* @var int
*/
protected $limit;
/**
* The current user's signature.
*
* @var string
*/
protected $signature;
/**
* Prefix to be on the request signature.
*
* @var string
*/
protected $prefix = 'concurrent:';
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $limit
* @return mixed
*/
public function handle($request, Closure $next, $limit = 1)
{
$this->limit = (int) $limit;
$this->setRequestSignature($request);
if ($this->limit <= Cache::get($this->signature)) {
throw new HttpResponseException(response()->json([
'error' => 'Too Many Attempts',
], Response::HTTP_FORBIDDEN));
}
$this->increment();
return $next($request);
}
/**
* Handle the outgoing response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
* @return mixed
*/
public function terminate($request, $response)
{
$this->setRequestSignature($request);
$this->decrement();
return $response;
}
/**
* Get the number of remaining concurrent requests the user can run.
*/
protected function getRemainingRequests(int $limit): int
{
return max(0, $limit - Cache::get($this->signature));
}
/**
* Manually set the signature for the current request.
*
* @param \Illuminate\Http\Request $request
* @param string|null $signature
* @return string
*/
public function setRequestSignature($request, $signature = null)
{
if (! empty($this->signature)) {
return $signature;
}
$signature = $this->prefix.sha1($signature ?? $this->resolveRequestSignature($request));
$this->signature = $signature;
return $signature;
}
/**
* Resolve the request signature for the current requesting user.
*
* @param \Illuminate\Http\Request $request
* @return string
*
* @throws \RuntimeException
*/
protected function resolveRequestSignature($request)
{
if (! empty($this->signature)) {
return $this->signature;
}
if ($user = $request->user()) {
return $user->getAuthIdentifier().'|'.$request->route()->uri();
}
if ($route = $request->route()) {
return $route->getDomain().'|'.$request->ip();
}
throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
}
/**
* Increment the count of currently running requests for the current user by 1.
*
* @return int
*/
protected function increment(): int
{
$value = 1;
if (Cache::has($this->signature)) {
$value = Cache::get($this->signature) + 1;
}
Cache::put($this->signature, $value, $this->cacheForSeconds);
return $value;
}
/**
* Decrement the count of currently running requests for the current user by 1.
*/
protected function decrement(): int
{
if (! Cache::has($this->signature)) {
return 0;
}
$value = Cache::get($this->signature) - 1;
if ($value === 0) {
Cache::forget($this->signature);
return 0;
}
Cache::put($this->signature, $value);
return $value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment