8000 [RateLimiter] Fix DateInterval normalization · symfony/symfony@56514ac · GitHub
[go: up one dir, main page]

Skip to content

Commit 56514ac

Browse files
committed
[RateLimiter] Fix DateInterval normalization
1 parent 97aedb3 commit 56514ac

File tree

3 files changed

+42
-2
lines changed

3 files changed

+42
-2
lines changed

src/Symfony/Component/RateLimiter/RateLimiterFactory.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ protected static function configureOptions(OptionsResolver $options): void
6969
{
7070
$intervalNormalizer = static function (Options $options, string $interval): \DateInterval {
7171
try {
72-
return (new \DateTimeImmutable())->diff(new \DateTimeImmutable('+'.$interval));
72+
// Force UTC timezone, because we don't want to deal with quirks happening when modifying dates in case
73+
// there is a default timezone with DST. Read more here https://github.com/symfony/symfony/pull/58757
74+
$now = \DateTimeImmutable::createFromFormat('U', time(), new \DateTimeZone('UTC'));
75+
76+
return $now->diff($now->modify('+'.$interval));
7377
} catch (\Exception $e) {
7478
if (!preg_match('/Failed to parse time string \(\+([^)]+)\)/', $e->getMessage(), $m)) {
7579
throw $e;

src/Symfony/Component/RateLimiter/Tests/RateLimiterFactoryTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\RateLimiter\Tests;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ClockMock;
1516
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
1617
use Symfony\Component\RateLimiter\Policy\FixedWindowLimiter;
1718
use Symfony\Component\RateLimiter\Policy\NoLimiter;
@@ -76,4 +77,39 @@ public static function invalidConfigProvider()
7677
'policy' => 'token_bucket',
7778
]];
7879
}
80+
81+
/**
82+
* This is a regression test that covers the bug described here https://github.com/symfony/symfony/pull/58757
83+
*
84+
* @group time-sensitive
85+
*/
86+
public function testExpirationTimeCalculationWhenUsingDefaultTimezoneRomeWithIntervalAfterCETChange()
87+
{
88+
$originalTimezone = date_default_timezone_get();
89+
try {
90+
// Timestamp for 'Sun 27 Oct 2024 12:59:40 AM UTC' that's jus 10000 t 20 seconds before switch CEST->CET
91+
ClockMock::withClockMock(1729990780);
92+
93+
// This is a prerequisite for the bug to happen
94+
date_default_timezone_set('Europe/Rome');
95+
96+
$storage = new InMemoryStorage();
97+
$factory = new RateLimiterFactory(
98+
[
99+
'id' => 'id_1',
100+
'policy' => 'fixed_window',
101+
'limit' => 30,
102+
'interval' => '21 seconds',
103+
],
104+
$storage
105+
);
106+
$rateLimiter = $factory->create('key');
107+
$rateLimiter->consume(1);
108+
$limiterState = $storage->fetch('id_1-key');
109+
// As expected the expiration is equal to the interval we defined
110+
$this->assertSame(21, $limiterState->getExpirationTime());
111+
} finally {
112+
date_default_timezone_set($originalTimezone);
113+
}
114+
}
79115
}

src/Symfony/Component/RateLimiter/Util/TimeUtil.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ final class TimeUtil
2020
{
2121
public static function dateIntervalToSeconds(\DateInterval $interval): int
2222
{
23-
$now = new \DateTimeImmutable();
23+
$now = \DateTimeImmutable::createFromFormat('U', time(), new \DateTimeZone('UTC'));
2424

2525
return $now->add($interval)->getTimestamp() - $now->getTimestamp();
2626
}

0 commit comments

Comments
 (0)
0