Laravel RateLimiter::clear() is not working for my endpoint throttle middleware
The Problem
I decided to implement throttle
middleware to prevent misuse of an endpoint.
However, I needed to customise it because I want to notifications to my Slack channel whenever this throttle is triggered.
So, I defined a custom rate limited in RouteServiceProvider
and used in the route file.
// RouteServiceProvider
RateLimiter::for('api-upload', function (Request $request) {
$userId = $request->user()->id;
$this->sendSlackAlert();
return Limit::perHour(10)->by($userId);
});
---
// Route file
Route::post('/upload', [UploadController::class, 'store'])
->middleware('throttle:api-upload');
When a user successfully performs a certain action without any abuse detected, I want to clear the throttle lock so the user may proceed without any hiccup. We cannot penalise a good and dedicated user, can we?
According to Laravel's documentation (Rate Limiting: Clearing Attempts ),
it's pretty straightforward. Just use RateLimiter::clear($key)
.
So, I tried just that but it doesn't work somehow. I tried several variations with no success.
// These don't work
RateLimiter::clear('api-upload:' . $userId);
RateLimiter::clear('api-upload' . $userId);
RateLimiter::clear('api-upload|' . $userId);
RateLimiter::clear('api-upload', $userId);
In fact, I tried checking the attempts and still got no explaination.
// These don't work
RateLimiter::attempts('api-upload:' . $userId); // returns 0
RateLimiter::clear('api-upload' . $userId); // returns 0
RateLimiter::clear('api-upload|' . $userId); // returns 0
RateLimiter::clear('api-upload', $userId); // returns 0
The user is still getting 429 (Too Many Attempts) responses, but I just couldn't find the rate limit data anywhere.
The Hidden Truth
After checking Laravel's official repository, I found out something interesting.
Laravel's ThrottleRequests
middleware automatically MD5 hashes named rate limiter keys.
From ThrottleRequests.php :
return $this->handleRequest(
$request,
$next,
Collection::wrap($limiterResponse)->map(function ($limit) use ($limiterName) {
return (object) [
'key' => self::$shouldHashKeys ? md5($limiterName.$limit->key) : $limiterName.':'.$limit->key,
// ...
];
})->all()
);
So, if your user ID is 12345
, the real cache key is actually md5('api-upload12345')
, instead of api-upload12345
.
Why RateLimiter::clear() Doesn't Work
The RateLimiter::clear()
method doesn't handle MD5 un-hashing.
The cleanRateLimiterKey()
method only removes HTML entities - no MD5 hashing.
From RateLimiter.php :
public function clear($key)
{
$key = $this->cleanRateLimiterKey($key);
$this->resetAttempts($key);
$this->cache->forget($key.':timer');
}
public function cleanRateLimiterKey($key)
{
return preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities($key));
}
The Solution
Use the MD5 hash when clearing:
// This works!
RateLimiter::clear(md5('api-upload' . $userId));
Verifying in Tinker
// Check attempts (wrong way)
RateLimiter::attempts('api-upload12345'); // 0
// Check attempts (correct way)
RateLimiter::attempts(md5('api-upload12345')); // 5
// Clear it
RateLimiter::clear(md5('api-upload12345'));
// Verify it's cleared
RateLimiter::attempts(md5('api-upload12345')); // 0
Helper Function
We can create a helper function to make our lives easier.
function clearNamedRateLimit($limiterName, $key) {
RateLimiter::clear(md5($limiterName . $key));
}
// Usage
clearNamedRateLimit('api-upload', $userId);
Key Takeaway
When clearing named rate limiters, always use:
RateLimiter::clear(md5($limiterName . $key));
I wonder if I should attempt to create an issue to Laravel repository to accept hashing strategy for the RateLimiter clear function.
public function clear($key, $hash = false)
{
if($hash){
$key = md5($key);
}
$key = $this->cleanRateLimiterKey($key);
$this->resetAttempts($key);
$this->cache->forget($key.':timer');
}
// Usage
RateLimiter::clear('api-upload' . $userId, true);
RateLimiter::clear('api-upload' . $userId);