Skip to content

Commit e8b4927

Browse files
HypeMCfabpot
authored andcommitted
[FrameworkBundle][HttpClient] Add ThrottlingHttpClient to limit requests within a timeframe
1 parent 4a65773 commit e8b4927

File tree

4 files changed

+110
-0
lines changed

4 files changed

+110
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `HttpOptions::setHeader()` to add or replace a single header
88
* Allow mocking `start_time` info in `MockResponse`
99
* Add `MockResponse::fromFile()` and `JsonMockResponse::fromFile()` methods to help using fixtures files
10+
* Add `ThrottlingHttpClient` to enable limiting the number request within a certain period
1011

1112
7.0
1213
---

Tests/ThrottlingHttpClientTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\HttpClient\Response\MockResponse;
17+
use Symfony\Component\HttpClient\ThrottlingHttpClient;
18+
use Symfony\Component\RateLimiter\RateLimiterFactory;
19+
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
20+
21+
class ThrottlingHttpClientTest extends TestCase
22+
{
23+
public function testThrottling()
24+
{
25+
$failPauseHandler = static function (float $duration) {
26+
self::fail(sprintf('The pause handler should\'t have been called, but it was called with %f.', $duration));
27+
};
28+
29+
$pauseHandler = static fn (float $expectedDuration) => function (float $duration) use ($expectedDuration) {
30+
self::assertEqualsWithDelta($expectedDuration, $duration, 1);
31+
};
32+
33+
$rateLimiterFactory = new RateLimiterFactory([
34+
'id' => 'token_bucket',
35+
'policy' => 'token_bucket',
36+
'limit' => 2,
37+
'rate' => ['interval' => '5 seconds', 'amount' => 2],
38+
], new InMemoryStorage());
39+
40+
$client = new ThrottlingHttpClient(
41+
new MockHttpClient([
42+
new MockResponse('', ['http_code' => 200, 'pause_handler' => $failPauseHandler]),
43+
new MockResponse('', ['http_code' => 200, 'pause_handler' => $failPauseHandler]),
44+
new MockResponse('', ['http_code' => 200, 'pause_handler' => $pauseHandler(5)]),
45+
new MockResponse('', ['http_code' => 200, 'pause_handler' => $pauseHandler(5)]),
46+
new MockResponse('', ['http_code' => 200, 'pause_handler' => $pauseHandler(10)]),
47+
]),
48+
$rateLimiterFactory->create(),
49+
);
50+
51+
$client->request('GET', 'http://example.com/foo');
52+
$client->request('GET', 'http://example.com/bar');
53+
$client->request('GET', 'http://example.com/baz');
54+
$client->request('GET', 'http://example.com/qux');
55+
$client->request('GET', 'http://example.com/corge');
56+
}
57+
}

ThrottlingHttpClient.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Symfony\Component\RateLimiter\LimiterInterface;
15+
use Symfony\Contracts\HttpClient\HttpClientInterface;
16+
use Symfony\Contracts\HttpClient\ResponseInterface;
17+
use Symfony\Contracts\Service\ResetInterface;
18+
19+
/**
20+
* Limits the number of requests within a certain period.
21+
*/
22+
class ThrottlingHttpClient implements HttpClientInterface, ResetInterface
23+
{
24+
use DecoratorTrait {
25+
reset as private traitReset;
26+
}
27+
28+
public function __construct(
29+
HttpClientInterface $client,
30+
private readonly LimiterInterface $rateLimiter,
31+
) {
32+
$this->client = $client;
33+
}
34+
35+
public function request(string $method, string $url, array $options = []): ResponseInterface
36+
{
37+
$response = $this->client->request($method, $url, $options);
38+
39+
if (0 < $waitDuration = $this->rateLimiter->reserve()->getWaitDuration()) {
40+
$response->getInfo('pause_handler')($waitDuration);
41+
}
42+
43+
return $response;
44+
}
45+
46+
public function reset(): void
47+
{
48+
$this->traitReset();
49+
$this->rateLimiter->reset();
50+
}
51+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"symfony/http-kernel": "^6.4|^7.0",
4141
"symfony/messenger": "^6.4|^7.0",
4242
"symfony/process": "^6.4|^7.0",
43+
"symfony/rate-limiter": "^6.4|^7.0",
4344
"symfony/stopwatch": "^6.4|^7.0"
4445
},
4546
"conflict": {

0 commit comments

Comments
 (0)