Skip to content

Commit

Permalink
Shipments: Handle API rate limit message (#14)
Browse files Browse the repository at this point in the history
* Shipments: Handle API rate limit message

* Update logic to throw exception

* Update logic to apply to all requests

* Use function to check rate limit

* Config: Add request limit option

* Update rate limit handling

* Revert removing retryAfter exception variable
  • Loading branch information
solflare authored Jul 21, 2022
1 parent a2bb15e commit 7395726
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 2 deletions.
1 change: 1 addition & 0 deletions config/shipengine.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'version' => env('SHIP_ENGINE_API_VERSION', 'v1'),
'base' => env('SHIP_ENGINE_ENDPOINT', 'https://api.shipengine.com/'),
],
'request_limit_per_minute' => env('SHIP_ENGINE_REQUEST_LIMIT_PER_MINUTE', 200),
'retries' => env('SHIP_ENGINE_RETRIES', 1),
'response' => [
'as_object' => env('SHIP_ENGINE_RESPONSE_AS_OBJECT', false),
Expand Down
35 changes: 34 additions & 1 deletion src/ShipEngineClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
use BluefynInternational\ShipEngine\Message\RateLimitExceededException;
use BluefynInternational\ShipEngine\Message\ShipEngineException;
use BluefynInternational\ShipEngine\Models\RequestLog;
use DateInterval;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Throwable;

class ShipEngineClient
Expand Down Expand Up @@ -184,10 +186,18 @@ private static function sendRequest(
}

try {
self::incrementRequestCount($config);

$response = $client->send(
$request,
['timeout' => $config->timeout->s, 'http_errors' => false]
);

$requestLogResponse = json_decode((string)$response->getBody(), true);

if (self::responseIsRateLimit($requestLogResponse)) {
throw new RateLimitExceededException(retryAfter: new DateInterval('PT1S'));
}
} catch (Exception|Throwable $err) {
if (config('shipengine.track_requests')) {
$requestLog->exception = substr($err->getMessage(), 0, config('shipengine.request_log_table_exception_length'));
Expand All @@ -205,7 +215,7 @@ private static function sendRequest(
}

$requestLog->response_code = $response->getStatusCode();
$requestLog->response = json_decode((string)$response->getBody(), true);
$requestLog->response = $requestLogResponse;

if (config('shipengine.track_requests')) {
$requestLog->save();
Expand Down Expand Up @@ -269,4 +279,27 @@ private static function deriveUserAgent(): string

return $sdk_version . ' ' . $os_kernel . ' ' . $php_version;
}

private static function responseIsRateLimit(array $response) : bool
{
return 'API rate limit exceeded' === ($response['message'] ?? null);
}

private static function incrementRequestCount(ShipEngineConfig $config) : void
{
$lock = tap(Cache::lock('shipengine.api-request.lock', 10))->block(10);

try {
$count = Cache::get('shipengine.api-request.count', 0);
$nextExpire = now()->seconds(0)->addMinute();

if ($count > $config->requestLimitPerMinute) {
throw new RateLimitExceededException(retryAfter: new DateInterval('PT1S'));
}

Cache::put('shipengine.api-request.count', $count + 1, $nextExpire);
} finally {
$lock->release();
}
}
}
7 changes: 7 additions & 0 deletions src/ShipEngineConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ final class ShipEngineConfig implements \JsonSerializable, Arrayable

public int $retries;

public int $requestLimitPerMinute;

public bool $asObject = false;

public DateInterval $timeout;
Expand All @@ -39,6 +41,11 @@ public function __construct(array $config = [])
$assert->isRetriesValid($retries);
$this->retries = $retries;

$requestLimitPerMinute = $config['request_limit_per_minute']
?? config('shipengine.request_limit_per_minute', 200);
$assert->isRequestLimitPerMinuteValid($requestLimitPerMinute);
$this->requestLimitPerMinute = $requestLimitPerMinute;

$timeout_value = $config['timeout'] ?? new DateInterval(config('shipengine.timeout', 'PT10S'));
if (is_string($timeout_value)) {
$timeout_value = new DateInterval($timeout_value);
Expand Down
5 changes: 4 additions & 1 deletion src/Traits/Shipments.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ public function createShipment(
$params,
);

if (! $response['has_errors'] && $config->asObject) {
if (
! ($response['has_errors'] ?? null)
&& $config->asObject
) {
$response['shipments'] = $this->listToObjects($response['shipments'], Shipment::class);
}

Expand Down
13 changes: 13 additions & 0 deletions src/Util/Assert.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,17 @@ public function isRetriesValid(int $retries) : void
);
}
}

public function isRequestLimitPerMinuteValid(int $requestsPerMinute) : void
{
if ($requestsPerMinute < 0) {
throw new ValidationException(
'Requests limit per minute must be zero or greater.',
null,
'shipengine',
'validation',
'invalid_field_value'
);
}
}
}

0 comments on commit 7395726

Please sign in to comment.