Lines 95.65% 22 / 23
Methods 50.00% 1 / 2
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 onKernelRequest 95.45% 21 / 22 0.00% 0 / 1 8
28readonly class ApiRateLimiterListener
29{
30    public function __construct(
31        private Configuration $configuration,
32        private RateLimiter $rateLimiter,
33    ) {
34    }
35
36    public function onKernelRequest(RequestEvent $event): void
37    {
38        if (!$event->isMainRequest()) {
39            return;
40        }
41
42        $request = $event->getRequest();
43
44        // Rate limiting curbs abusive, state-changing requests (login attempts,
45        // registration, comments, ...). It must not throttle:
46        //  - safe/idempotent reads (GET/HEAD/OPTIONS), e.g., config endpoints, or
47        //  - authenticated "private" API routes used by logged-in users.
48        if ($request->isMethodSafe()) {
49            return;
50        }
51
52        $route = (string) $request->attributes->get('_route', '');
53        if (str_starts_with($route, 'api.private.')) {
54            return;
55        }
56
57        $requestLimit = (int) $this->configuration->get('api.rateLimit.requests');
58        $interval = (int) $this->configuration->get('api.rateLimit.interval');
59
60        if ($requestLimit < 1 || $interval < 1) {
61            return;
62        }
63
64        $clientIdentifier = $request->getClientIp() ?? 'anonymous';
65
66        if ($this->rateLimiter->check($clientIdentifier, $requestLimit, $interval)) {
67            return;
68        }
69
70        $response = new JsonResponse(data: [
71            'error' => 'Too many requests.',
72            'message' => 'Rate limit exceeded. Please retry later.',
73        ], status: Response::HTTP_TOO_MANY_REQUESTS);
74
75        foreach ($this->rateLimiter->getHeaders() as $header => $value) {
76            $response->headers->set($header, (string) $value);
77        }
78
79        $event->setResponse($response);
80    }
81}