Search code examples
laravellaravel-routingrate-limiting

Unable to get the actual remaining time from rate limiter


I've created a rate limiter for login which limits the request to n times per 10 minutes. If a request has reach the rate limit, I want to return a validation exception informing the remaining time before the rate limiter is cleared.

For simplicity, I set the limit to just 2, as follows:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('login', function (Request $request) {
    $email = (string) $request->email;
    $throttle_key = str($email)->ascii()->lower().'|'.$request->ip();

    return Limit::perMinutes(10, 2)
            ->by($throttle_key)
            ->response(function () use ($throttle_key) {
                $seconds = RateLimiter::availableIn($throttle_key);

                throw ValidationException::withMessages([
                    'email' => trans('auth.throttle', [
                        'seconds' => $seconds
                    ]),
                ]);
            });
});

The code above is limiting the request correctly: users only has 2 attempts within 10 minutes to try to login using their email. However, the RateLimiter::availableIn($throttle_key) is not giving the correct value as I mentioned above.

When I tried to login incorrectly for the 3rd time, instead of getting the value between 1 to 600 seconds remaining, the RateLimiter::availableIn($throttle_key) only gives 1-60. After passing the 1st minute of the decay time, it always showing 0 second remains, but the rate limiter is still holding the request as it should for the next 9 minutes.

Please share any suggestion that comes to your mind. Your help is much appreciated.


Solution

  • I am having the same problem, but I tinkered around and In the Illuminate\Cache\RateLimiter class where the facade is coming from did some dumps,

    Apparently it is storing it in the cache with some weird names, for example my key was '1' and it had stored it in a cryptographic manner something like 'sdkf34kfshsf:timer' in the cache, and accessing it via the method availableIn was passing just '1' to the cache and not the encrypted value.

    A workaround I have found is to use the value from the header to calculate the remaining time.

    In the callback function of the response method add the headers array.

    ->response(function(Request $request,array $headers) {
    

    and in the 'seconds' line

    'seconds' => $headers['Retry-After']
    

    OR

    'seconds' => $headers['X-RateLimit-Reset'] - time()
    

    the difference should provide the seconds remaining