master

laravel/framework

Last updated at: 29/12/2023 09:24

ThrottleRequestsWithRedis.php

TLDR

This file, ThrottleRequestsWithRedis.php, is part of the Illuminate\Routing\Middleware namespace in the Laravel framework. It provides a request throttling middleware that uses Redis for storing and managing request limits.

Methods

handleRequest

This method handles an incoming request by checking if the request has exceeded the specified limits. If the request has exceeded the limit, it throws a ThrottleRequestsException. Otherwise, it adds headers to the response with the maximum number of attempts and the remaining attempts.

tooManyAttempts

This method determines if the given key has been accessed too many times by using the DurationLimiter class from the Illuminate\Redis\Limiters namespace. It returns true if the limiter is unable to acquire the lock, indicating that the request has exceeded the limit.

calculateRemainingAttempts

This method calculates the number of remaining attempts based on the given key and maxAttempts. If a retryAfter value is provided, it returns 0.

getTimeUntilNextRetry

This method calculates the number of seconds until the lock is released for the given key. It subtracts the current time from the decaysAt timestamp.

getRedisConnection

This method returns the Redis connection that should be used for throttling.

Classes

ThrottleRequestsWithRedis

This class extends the ThrottleRequests class and implements the request throttling functionality using Redis. It maintains properties to store the Redis factory implementation, the decaysAt timestamp for each key, and the number of remaining slots for each key. It overrides the handleRequest method to handle the request throttling logic.

<?php

namespace Illuminate\Routing\Middleware;

use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Redis\Limiters\DurationLimiter;

class ThrottleRequestsWithRedis extends ThrottleRequests
{
    /**
     * The Redis factory implementation.
     *
     * @var \Illuminate\Contracts\Redis\Factory
     */
    protected $redis;

    /**
     * The timestamp of the end of the current duration by key.
     *
     * @var array
     */
    public $decaysAt = [];

    /**
     * The number of remaining slots by key.
     *
     * @var array
     */
    public $remaining = [];

    /**
     * Create a new request throttler.
     *
     * @param  \Illuminate\Cache\RateLimiter  $limiter
     * @param  \Illuminate\Contracts\Redis\Factory  $redis
     * @return void
     */
    public function __construct(RateLimiter $limiter, Redis $redis)
    {
        parent::__construct($limiter);

        $this->redis = $redis;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  array  $limits
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
     */
    protected function handleRequest($request, Closure $next, array $limits)
    {
        foreach ($limits as $limit) {
            if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) {
                throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
            }
        }

        $response = $next($request);

        foreach ($limits as $limit) {
            $response = $this->addHeaders(
                $response,
                $limit->maxAttempts,
                $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
            );
        }

        return $response;
    }

    /**
     * Determine if the given key has been "accessed" too many times.
     *
     * @param  string  $key
     * @param  int  $maxAttempts
     * @param  int  $decaySeconds
     * @return mixed
     */
    protected function tooManyAttempts($key, $maxAttempts, $decaySeconds)
    {
        $limiter = new DurationLimiter(
            $this->getRedisConnection(), $key, $maxAttempts, $decaySeconds
        );

        return tap(! $limiter->acquire(), function () use ($key, $limiter) {
            [$this->decaysAt[$key], $this->remaining[$key]] = [
                $limiter->decaysAt, $limiter->remaining,
            ];
        });
    }

    /**
     * Calculate the number of remaining attempts.
     *
     * @param  string  $key
     * @param  int  $maxAttempts
     * @param  int|null  $retryAfter
     * @return int
     */
    protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
    {
        return is_null($retryAfter) ? $this->remaining[$key] : 0;
    }

    /**
     * Get the number of seconds until the lock is released.
     *
     * @param  string  $key
     * @return int
     */
    protected function getTimeUntilNextRetry($key)
    {
        return $this->decaysAt[$key] - $this->currentTime();
    }

    /**
     * Get the Redis connection that should be used for throttling.
     *
     * @return \Illuminate\Redis\Connections\Connection
     */
    protected function getRedisConnection()
    {
        return $this->redis->connection();
    }
}