LaravelのThrottleRequestのコードリーディングメモ。
今回はこんな感じなコードで何が起きているかを見ます。
Route::middleware(['throttle:api'])->group(function () {
// ...
});
/**
* Bootstrap any application services.
*/
protected function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
throttle
は \Illuminate\Routing\Middleware\ThrottleRequests
のエイリアスなのでそこから見ていきます。
throttle:api
のコロンのあとは引数になるので、以下の $maxAttempts
は api
が入ります。
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '')
{
if (is_string($maxAttempts)
&& func_num_args() === 3
&& ! is_null($limiter = $this->limiter->limiter($maxAttempts))) {
return $this->handleRequestUsingNamedLimiter($request, $next, $maxAttempts, $limiter);
}
return $this->handleRequest(
$request,
$next,
[
(object) [
'key' => $prefix.$this->resolveRequestSignature($request),
'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts),
'decaySeconds' => 60 * $decayMinutes,
'responseCallback' => null,
],
]
);
}
throttle:api
の場合は最初のif分岐に入ります。
handleRequestUsingNamedLimiter()
は $limiter()
を呼び出し RateLimiter::for()
で定義したClosureの戻り値を取得し、その設定を使ってリクエストの閾値チェックを行っています。
protected function handleRequestUsingNamedLimiter($request, Closure $next, $limiterName, Closure $limiter)
{
$limiterResponse = $limiter($request);
if ($limiterResponse instanceof Response) {
return $limiterResponse;
} elseif ($limiterResponse instanceof Unlimited) {
return $next($request);
}
return $this->handleRequest(
$request,
$next,
collect(Arr::wrap($limiterResponse))->map(function ($limit) use ($limiterName) {
return (object) [
'key' => self::$shouldHashKeys ? md5($limiterName.$limit->key) : $limiterName.':'.$limit->key,
'maxAttempts' => $limit->maxAttempts,
'decaySeconds' => $limit->decaySeconds,
'responseCallback' => $limit->responseCallback,
];
})->all()
);
}
今回の例だと以下のような設定値となっています。
$limit->key
=> ‘api’ +ログインユーザあるいはIPアドレス
$limit->maxAttempts
=> 60$limit->decaySeconds
=> 60
RateLimiter::tooManyAttempts()
でチェックを行い閾値を超えていたらエラーを投げます。閾値を超えていない場合は RateLimiter::hit()
でリクエスト数を更新します。
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
$this->limiter->hit($limit->key, $limit->decaySeconds);
}
$response = $next($request);
// ...(省略)...
return $response;
}
RateLimiter::tooManyAttempts()
ではキャッシュストレージ内にあるリクエスト数が閾値を超えていたらtrue、そうでなければキャッシュストレージ内のリクエスト数を0にしてfalseを返します。
public function tooManyAttempts($key, $maxAttempts)
{
if ($this->attempts($key) >= $maxAttempts) {
if ($this->cache->has($this->cleanRateLimiterKey($key).':timer')) {
return true;
}
$this->resetAttempts($key);
}
return false;
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Cache/RateLimiter.php#L94-L105 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Cache/RateLimiter.php#L152-L157
RateLimiter::hit()
は有効期限つきでリクエスト数をインクリメントします。
public function hit($key, $decaySeconds = 60)
{
return $this->increment($key, $decaySeconds);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Cache/RateLimiter.php#L114-L117