Skip to content

Commit 65abbd0

Browse files
committed
feat: add possibility to fail open on storage error
Changelog: * Add possiblity to configure to skip rate limiting on storage error. Can be configured globally by setting the `noxlogic_rate_limit.fail_open` parameter and/or per `#[RateLimit(failOpen: true)]` Attribute * Added `RateLimitStorageExceptionInterface` as possible exception to `StorageInterface` methods
1 parent 057ad74 commit 65abbd0

32 files changed

Lines changed: 1033 additions & 216 deletions

Attribute/RateLimit.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ public function __construct(
2626
/**
2727
* @var mixed Generic payload
2828
*/
29-
public mixed $payload = null
29+
public mixed $payload = null,
30+
31+
/**
32+
* @var bool|null Defines if the rate limiter blocks the request when a technical problem occurs (default).
33+
* For example, when the Redis database which is used as a rate limit storage is down.
34+
* If set to `false`, the request is allowed to proceed even if the rate limiter cannot determine if the rate limit has been exceeded.
35+
* `null` means that the globally-configured default should be used
36+
*/
37+
public ?bool $failOpen = null
3038
) {
3139
// @RateLimit annotation used to support single method passed as string, keep that for retrocompatibility
3240
if (!is_array($methods)) {
@@ -75,4 +83,9 @@ public function setPayload(mixed $payload): void
7583
{
7684
$this->payload = $payload;
7785
}
86+
87+
public function setFailOpen(bool $failOpen): void
88+
{
89+
$this->failOpen = $failOpen;
90+
}
7891
}

DependencyInjection/Configuration.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ public function getConfigTreeBuilder(): TreeBuilder
135135
->end()
136136
->end()
137137
->end()
138+
->booleanNode('fail_open')
139+
->defaultFalse()
140+
->treatNullLike(false)
141+
->info('Defines if the rate limiter blocks the request when a technical problem occurs')
142+
->end()
138143
->booleanNode('fos_oauth_key_listener')
139144
->defaultTrue()
140145
->info('Enabled the FOS OAuthServerBundle listener')

DependencyInjection/NoxlogicRateLimitExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ private function loadServices(ContainerBuilder $container, array $config): void
4545

4646
$container->setParameter('noxlogic_rate_limit.path_limits', $config['path_limits']);
4747

48+
$container->setParameter('noxlogic_rate_limit.fail_open', $config['fail_open']);
49+
4850
switch ($config['storage_engine']) {
4951
case 'memcache':
5052
$container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\Memcache');

EventListener/RateLimitAnnotationListener.php

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
88
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
99
use Noxlogic\RateLimitBundle\Exception\RateLimitExceptionInterface;
10+
use Noxlogic\RateLimitBundle\Exception\Storage\RateLimitStorageExceptionInterface;
1011
use Noxlogic\RateLimitBundle\Service\RateLimitService;
1112
use Noxlogic\RateLimitBundle\Util\PathLimitProcessor;
1213
use Symfony\Component\HttpFoundation\Request;
@@ -33,6 +34,9 @@ public function __construct(
3334
$this->pathLimitProcessor = $pathLimitProcessor;
3435
}
3536

37+
/**
38+
* @throws RateLimitStorageExceptionInterface
39+
*/
3640
public function onKernelController(ControllerEvent $event): void
3741
{
3842
// Skip if the bundle isn't enabled (for instance in test environment)
@@ -68,31 +72,39 @@ public function onKernelController(ControllerEvent $event): void
6872

6973
$key = $this->getKey($event, $rateLimit, $rateLimits);
7074

71-
// Ratelimit the call
72-
$rateLimitInfo = $this->rateLimitService->limitRate($key);
73-
if (! $rateLimitInfo) {
74-
// Create new rate limit entry for this call
75-
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
75+
$shouldFailOpenOnStorageError = $rateLimit->failOpen ?? $this->getParameter('fail_open', false);
76+
try {
77+
// Ratelimit the call
78+
$rateLimitInfo = $this->rateLimitService->limitRate($key);
7679
if (! $rateLimitInfo) {
77-
// @codeCoverageIgnoreStart
78-
return;
79-
// @codeCoverageIgnoreEnd
80+
// Create new rate limit entry for this call
81+
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
82+
if (! $rateLimitInfo) {
83+
// @codeCoverageIgnoreStart
84+
return;
85+
// @codeCoverageIgnoreEnd
86+
}
8087
}
81-
}
82-
8388

84-
// Store the current rating info in the request attributes
85-
$request->attributes->set('rate_limit_info', $rateLimitInfo);
86-
87-
// Reset the rate limits
88-
if(time() >= $rateLimitInfo->getResetTimestamp()) {
89-
$this->rateLimitService->resetRate($key);
90-
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
91-
if (! $rateLimitInfo) {
92-
// @codeCoverageIgnoreStart
89+
// Store the current rating info in the request attributes
90+
$request->attributes->set('rate_limit_info', $rateLimitInfo);
91+
92+
// Reset the rate limits
93+
if(time() >= $rateLimitInfo->getResetTimestamp()) {
94+
$this->rateLimitService->resetRate($key);
95+
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
96+
if (! $rateLimitInfo) {
97+
// @codeCoverageIgnoreStart
98+
return;
99+
// @codeCoverageIgnoreEnd
100+
}
101+
}
102+
} catch (RateLimitStorageExceptionInterface $storageException) {
103+
if ($shouldFailOpenOnStorageError) {
93104
return;
94-
// @codeCoverageIgnoreEnd
95105
}
106+
107+
throw $storageException;
96108
}
97109

98110
// When we exceeded our limit, return a custom error response
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Noxlogic\RateLimitBundle\Exception\Storage;
5+
6+
/**
7+
* @internal BC promise does not cover this class. Do not use directly
8+
*/
9+
final class CreateRateRateLimitStorageException extends RateLimitStorageException
10+
{
11+
public function __construct(\Throwable $previous)
12+
{
13+
parent::__construct('Failed to create rate limit', $previous);
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Noxlogic\RateLimitBundle\Exception\Storage;
5+
6+
/**
7+
* @internal BC promise does not cover this class. Do not use directly
8+
*/
9+
final class GetRateInfoRateLimitStorageException extends RateLimitStorageException
10+
{
11+
public function __construct(\Throwable $previous)
12+
{
13+
parent::__construct('Failed to get rate limit info', $previous);
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Noxlogic\RateLimitBundle\Exception\Storage;
5+
6+
/**
7+
* @internal BC promise does not cover this class. Do not use directly
8+
*/
9+
final class LimitRateRateLimitStorageException extends RateLimitStorageException
10+
{
11+
public function __construct(\Throwable $previous)
12+
{
13+
parent::__construct('Failed to apply rate limit', $previous);
14+
}
15+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Noxlogic\RateLimitBundle\Exception\Storage;
5+
6+
/**
7+
* @internal BC promise does not cover this class. Do not use directly
8+
*/
9+
abstract class RateLimitStorageException extends \Exception implements RateLimitStorageExceptionInterface
10+
{
11+
public function __construct(string $problemDescription, \Throwable $previous)
12+
{
13+
$message = \sprintf('Rate limit storage: %s', $problemDescription);
14+
15+
if ($previous->getMessage()) {
16+
$message .= \sprintf(': %s', $previous->getMessage());
17+
}
18+
19+
parent::__construct($message, previous: $previous);
20+
}
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Noxlogic\RateLimitBundle\Exception\Storage;
5+
6+
/**
7+
* A technical problem happened at the storage-level
8+
*/
9+
interface RateLimitStorageExceptionInterface extends \Throwable
10+
{
11+
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Noxlogic\RateLimitBundle\Exception\Storage;
5+
6+
/**
7+
* @internal BC promise does not cover this class. Do not use directly
8+
*/
9+
final class ResetRateRateLimitStorageException extends RateLimitStorageException
10+
{
11+
public function __construct(\Throwable $previous)
12+
{
13+
parent::__construct('Failed to reset rate', $previous);
14+
}
15+
}

0 commit comments

Comments
 (0)