Skip to content
Open
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
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,28 +56,36 @@ $config->setDeveloperApiKey('*');
$hubspot = \HubSpot\Factory::create(null, $config);
```

#### API Client comes with Middleware for implementation of Rate and Concurrent Limiting
#### Retry Middleware

It provides an ability to turn on retry for failed requests with statuses 429 or 500. Please note that Apps using OAuth are only subject to a limit of 100 requests every 10 seconds.
The API client provides retry middleware for three failure scenarios: rate limiting (429), internal server errors (500–503, 520–599), and connection errors. All middleware retry up to `RetryMiddlewareFactory::DEFAULT_MAX_RETRIES` (5) times by default.

If no delay function is provided, Guzzle applies its built-in exponential backoff. Available delay helpers:

- `Delay::getConstantDelayFunction(int $secondsDelay = 10)` — fixed delay between retries
- `Delay::getLinearDelayFunction()` — delay grows linearly with retry count

Please note that Apps using OAuth are only subject to a limit of 100 requests every 10 seconds.

```php
$handlerStack = \GuzzleHttp\HandlerStack::create();

// Retry on 429 with a constant 10-second delay
$handlerStack->push(
\HubSpot\RetryMiddlewareFactory::createRateLimitMiddleware(
\HubSpot\Delay::getConstantDelayFunction()
)
);

// Retry on 5xx errors (exponential backoff by default)
$handlerStack->push(
\HubSpot\RetryMiddlewareFactory::createInternalErrorsMiddleware(
\HubSpot\Delay::getExponentialDelayFunction(2)
)
\HubSpot\RetryMiddlewareFactory::createInternalErrorsMiddleware()
);

// Retry on connection errors for cURL codes 52, 55, 56 (exponential backoff by default)
// Pass an empty array to retry all ConnectExceptions regardless of cURL error code
$handlerStack->push(
\HubSpot\RetryMiddlewareFactory::createConnectionErrorsMiddleware(
\HubSpot\Delay::getExponentialDelayFunction(2)
)
\HubSpot\RetryMiddlewareFactory::createConnectionErrorsMiddleware()
);

$client = new \GuzzleHttp\Client(['handler' => $handlerStack]);
Expand Down
3 changes: 3 additions & 0 deletions lib/Delay.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public static function getLinearDelayFunction()
};
}

/**
* @deprecated pass null as the delay function instead — Guzzle will apply its built-in exponential delay via \GuzzleHttp\RetryMiddleware::exponentialDelay
*/
public static function getExponentialDelayFunction(int $base)
{
return function ($retries) use ($base) {
Expand Down
12 changes: 8 additions & 4 deletions lib/RetryMiddlewareFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static function createConnectionErrorsMiddleware(
array $curlErrorCodes = self::TRANSIENT_CURL_ERROR_CODES
): callable {
return Middleware::retry(
static::getRetryFunctionByConnectionErrors($maxRetries, $curlErrorCodes),
static::getRetryFunctionByConnectionErrors($curlErrorCodes, $maxRetries),
$delayFunction
);
}
Expand Down Expand Up @@ -146,8 +146,8 @@ public static function getRetryFunction(
}

public static function getRetryFunctionByConnectionErrors(
int $maxRetries = self::DEFAULT_MAX_RETRIES,
array $curlErrorCodes = self::TRANSIENT_CURL_ERROR_CODES
array $curlErrorCodes = self::TRANSIENT_CURL_ERROR_CODES,
int $maxRetries = self::DEFAULT_MAX_RETRIES
): callable {
return function (
$retries,
Expand All @@ -163,14 +163,18 @@ public static function getRetryFunctionByConnectionErrors(
return false;
}

if (empty($curlErrorCodes)) {
return true;
}

$handlerContext = $exception->getHandlerContext();
$errno = $handlerContext['errno'] ?? null;

if (is_numeric($errno) && in_array((int) $errno, $curlErrorCodes, true)) {
return true;
}

if (preg_match('/cURL error\s+(\d+):/i', $exception->getMessage(), $matches) === 1) {
if (1 === preg_match('/cURL error\s+(\d+):/i', $exception->getMessage(), $matches)) {
return in_array((int) $matches[1], $curlErrorCodes, true);
}

Expand Down
8 changes: 4 additions & 4 deletions tests/Unit/RetryMiddlewareFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class RetryMiddlewareFactoryTest extends TestCase
/** @test */
public function itRetriesRetriableConnectionErrorsByErrno(): void
{
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(3);
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(RetryMiddlewareFactory::TRANSIENT_CURL_ERROR_CODES, 3);
$request = new Request('GET', 'https://api.hubapi.com/test');
$exception = new ConnectException(
'cURL error 56: OpenSSL SSL_read unexpected eof while reading',
Expand All @@ -32,7 +32,7 @@ public function itRetriesRetriableConnectionErrorsByErrno(): void
/** @test */
public function itRetriesRetriableConnectionErrorsByMessageWhenErrnoMissing(): void
{
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(3);
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(RetryMiddlewareFactory::TRANSIENT_CURL_ERROR_CODES, 3);
$request = new Request('GET', 'https://api.hubapi.com/test');
$exception = new ConnectException(
'cURL error 55: Send failure: Broken pipe',
Expand All @@ -45,7 +45,7 @@ public function itRetriesRetriableConnectionErrorsByMessageWhenErrnoMissing(): v
/** @test */
public function itDoesNotRetryNonRetriableConnectionErrors(): void
{
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(3);
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(RetryMiddlewareFactory::TRANSIENT_CURL_ERROR_CODES, 3);
$request = new Request('GET', 'https://api.hubapi.com/test');
$exception = new ConnectException(
'cURL error 60: SSL certificate problem',
Expand All @@ -60,7 +60,7 @@ public function itDoesNotRetryNonRetriableConnectionErrors(): void
/** @test */
public function itStopsRetryingWhenMaxRetriesReached(): void
{
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(1);
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(RetryMiddlewareFactory::TRANSIENT_CURL_ERROR_CODES, 1);
$request = new Request('GET', 'https://api.hubapi.com/test');
$exception = new ConnectException(
'cURL error 56: OpenSSL SSL_read unexpected eof while reading',
Expand Down
Loading