From e9ac4f6150c990a76d2ba9a949f4f89fb4681025 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Tue, 25 Jun 2024 16:08:56 +0200 Subject: [PATCH] Respond with HTTP 429 when receiving too many requests Previously a 500 error was thrown which was not really the correct behaviour. --- .../ApplyRateLimitingSubscriber.php | 8 +++++++- .../ApplyRateLimitingSubscriberTest.php | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/RateLimiting/ApplyRateLimitingSubscriber.php b/src/RateLimiting/ApplyRateLimitingSubscriber.php index 2cc3d7d..0af664a 100644 --- a/src/RateLimiting/ApplyRateLimitingSubscriber.php +++ b/src/RateLimiting/ApplyRateLimitingSubscriber.php @@ -6,6 +6,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\RateLimiter\RateLimiterFactory; @@ -48,6 +49,11 @@ private function ensureRateLimiting(Request $request, RateLimiterFactory $rateLi $limiterKey = sprintf('rate_limit_ip_%s', $request->getClientIp()); $limit = $rateLimiter->create($limiterKey)->consume(); $request->attributes->set('rate_limit', $limit); - $limit->ensureAccepted(); + + if (false === $limit->isAccepted()) { + throw new TooManyRequestsHttpException( + $limit->getRetryAfter()->format(\DateTimeInterface::RFC7231) + ); + } } } diff --git a/tests/RateLimiting/ApplyRateLimitingSubscriberTest.php b/tests/RateLimiting/ApplyRateLimitingSubscriberTest.php index 1492d5e..26482ec 100644 --- a/tests/RateLimiting/ApplyRateLimitingSubscriberTest.php +++ b/tests/RateLimiting/ApplyRateLimitingSubscriberTest.php @@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\RateLimiter\RateLimit; @@ -19,6 +20,8 @@ #[CoversClass(ApplyRateLimitingSubscriber::class)] class ApplyRateLimitingSubscriberTest extends TestCase { + private const MAX_PER_PERIOD = 2; + #[Test] public function get_subscribed_events_returns_correct_event_and_priority(): void { @@ -88,6 +91,19 @@ public function ensure_rate_limiting_sets_rate_limit_attribute(): void $this->assertTrue($rateLimit->isAccepted()); } + #[Test] + public function ensure_http_429_is_returned_after_too_many_requests(): void + { + [, $event] = $this->createControllerEvent(); + $sut = new ApplyRateLimitingSubscriber($this->getRateLimiterClassMap()); + + $this->expectException(TooManyRequestsHttpException::class); + + for ($i = 0; $i <= self::MAX_PER_PERIOD; $i++) { + $sut->onKernelController($event); + } + } + /** * @return array{Request, ControllerEvent} */ @@ -114,9 +130,9 @@ private function getRateLimiterClassMap(): array [ 'id' => 'test', 'policy' => 'token_bucket', - 'limit' => 10, + 'limit' => self::MAX_PER_PERIOD, 'rate' => [ - 'interval' => '1 minute', + 'interval' => '10 seconds', ], ], new InMemoryStorage()