Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/Helpers/LimitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Saloon\RateLimitPlugin\Helpers;

use Closure;
use Saloon\RateLimitPlugin\Limit;
use Saloon\RateLimitPlugin\Exceptions\LimitException;

Expand All @@ -17,7 +16,7 @@ class LimitHelper
* @return array<Limit>
* @throws LimitException
*/
public static function configureLimits(array $limits, ?string $prefix, ?Closure $tooManyAttemptsHandler = null): array
public static function configureLimits(array $limits, ?string $prefix, ?Limit $tooManyAttemptsLimit = null): array
{
// Firstly, we will clean up the limits array to only ensure the `Limit` classes
// are being processed.
Expand All @@ -33,8 +32,8 @@ public static function configureLimits(array $limits, ?string $prefix, ?Closure
// Next we will append our "too many attempts" limit which will be used when
// the response actually hits a 429 status.

if (isset($tooManyAttemptsHandler)) {
$limits[] = Limit::custom($tooManyAttemptsHandler)->name('too_many_attempts_limit');
if (isset($tooManyAttemptsLimit)) {
$limits[] = $tooManyAttemptsLimit->name('too_many_attempts_limit');
}

// Next we will set the prefix on each of the limits.
Expand Down
28 changes: 24 additions & 4 deletions src/Traits/HasRateLimits.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function bootHasRateLimits(PendingRequest $pendingRequest): void
}
});

$pendingRequest->middleware()->onResponse(function (Response $response): void {
$pendingRequest->middleware()->onResponse(function (Response $response): Response {
$limitThatWasExceeded = null;
$store = $this->rateLimitStore();

Expand Down Expand Up @@ -92,8 +92,18 @@ public function bootHasRateLimits(PendingRequest $pendingRequest): void
// place. We should make sure to throw the exception here.

if (isset($limitThatWasExceeded)) {
$this->throwLimitException($limitThatWasExceeded);
if (! $limit->getShouldSleep()) {
$this->throwLimitException($limitThatWasExceeded);
}

// When the limit has been instructed to sleep() we will make the request
// again, which will trigger the request middleware to sleep, and then
// hopefully get a successful response afterward.

return $this->send($response->getRequest());
}

return $response;
}, order: PipeOrder::FIRST);
}

Expand All @@ -105,6 +115,16 @@ protected function getLimiterPrefix(): ?string
return (new ReflectionClass($this))->getShortName();
}

/**
* Define the "Too Many Attempts" (429) limiter
*
* This limiter will automatically attempt to detect 429 requests.
*/
protected function getTooManyAttemptsLimiter(): ?Limit
{
return Limit::custom($this->handleTooManyAttempts(...));
}

/**
* Handle too many attempts (429) statuses
*/
Expand Down Expand Up @@ -209,9 +229,9 @@ public function rateLimitStore(): RateLimitStore
*/
public function getLimits(): array
{
$tooManyAttemptsHandler = $this->detectTooManyAttempts === true ? $this->handleTooManyAttempts(...) : null;
$tooManyAttemptsLimit = $this->detectTooManyAttempts === true ? $this->getTooManyAttemptsLimiter() : null;

return LimitHelper::configureLimits($this->resolveLimits(), $this->getLimiterPrefix(), $tooManyAttemptsHandler);
return LimitHelper::configureLimits($this->resolveLimits(), $this->getLimiterPrefix(), $tooManyAttemptsLimit);
}

/**
Expand Down
26 changes: 24 additions & 2 deletions tests/Feature/HasRateLimitsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Saloon\Exceptions\Request\Statuses\InternalServerErrorException;
use Saloon\RateLimitPlugin\Tests\Fixtures\Requests\LimitedSoloRequest;
use Saloon\RateLimitPlugin\Tests\Fixtures\Connectors\CustomPrefixConnector;
use Saloon\RateLimitPlugin\Tests\Fixtures\Connectors\SleepTooManyRequestsConnector;
use Saloon\RateLimitPlugin\Tests\Fixtures\Connectors\CustomTooManyRequestsConnector;
use Saloon\RateLimitPlugin\Tests\Fixtures\Connectors\DisabledTooManyRequestsConnector;

Expand Down Expand Up @@ -173,11 +174,11 @@
'timestamp' => $currentTimestampPlusFive,
]);

// Now when we make this request, it should pause the application for 10 seconds
// Now when we make this request, it should pause the application for 5 seconds

$connector->send(new UserRequest);

expect(time())->toEqual($currentTimestampPlusFive);
expect(time())->toBeGreaterThanOrEqual($currentTimestampPlusFive);
});

test('you can create a limiter that listens for 429 and will automatically back off for the Retry-After duration', function () {
Expand Down Expand Up @@ -293,6 +294,27 @@
expect($store->getStore())->toBeEmpty();
});

test('you can customise the 429 error detection to sleep instead', function () {
$store = new MemoryStore;
$connector = new SleepTooManyRequestsConnector($store, []);

$connector->withMockClient(new MockClient([
new MockResponse(['name' => 'Sam'], 200),
MockResponse::make(['status' => 'Too Many Requests'], 429),
MockResponse::make(['name' => 'Jon'], 200),
]));

$startTime = time();

$connector->send(new UserRequest);

$response = $connector->send(new UserRequest);

expect($response->json())->toBe(['name' => 'Jon']);

expect(time())->toBeGreaterThanOrEqual($startTime + 5);
});

test('the rate limiter can be used on a request', function () {
$store = new MemoryStore;

Expand Down
28 changes: 28 additions & 0 deletions tests/Fixtures/Connectors/SleepTooManyRequestsConnector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Saloon\RateLimitPlugin\Tests\Fixtures\Connectors;

use Saloon\Http\Response;
use Saloon\RateLimitPlugin\Limit;

class SleepTooManyRequestsConnector extends TestConnector
{
protected function getTooManyAttemptsLimiter(): ?Limit
{
return Limit::custom($this->handleTooManyAttempts(...))->sleep();
}

/**
* Handle too many attempts (429) statuses
*/
protected function handleTooManyAttempts(Response $response, Limit $limit): void
{
if ($response->status() !== 429) {
return;
}

$limit->exceeded(releaseInSeconds: 5);
}
}