From c5677dad42cdd5b65544dba7c7c987d75d50eb89 Mon Sep 17 00:00:00 2001 From: ksvirkou-hubspot Date: Mon, 11 May 2026 16:47:10 +0300 Subject: [PATCH 1/3] Improve retry middleware and update documentation - `getRetryFunctionByConnectionErrors`: swapped param order so `$curlErrorCodes` comes first, consistent with how it is the primary customization point - Passing an empty `$curlErrorCodes` now retries all `ConnectException`s unconditionally, rather than silently skipping them - `getExponentialDelayFunction` deprecated in favour of passing `null` and letting Guzzle apply its built-in exponential backoff - README section renamed from "Rate and Concurrent Limiting" to "Retry Middleware", covering all three middleware, default retry count, available delay helpers, and the empty-array behaviour Co-Authored-By: Claude Sonnet 4.6 (1M context) --- README.md | 24 ++++++++++++++++-------- lib/Delay.php | 3 +++ lib/RetryMiddlewareFactory.php | 10 +++++++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 99bd5bc14..ec4b34783 100644 --- a/README.md +++ b/README.md @@ -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]); diff --git a/lib/Delay.php b/lib/Delay.php index 242627996..12f05dd7e 100644 --- a/lib/Delay.php +++ b/lib/Delay.php @@ -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) { diff --git a/lib/RetryMiddlewareFactory.php b/lib/RetryMiddlewareFactory.php index e78bf72ff..81f2ba172 100644 --- a/lib/RetryMiddlewareFactory.php +++ b/lib/RetryMiddlewareFactory.php @@ -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 ); } @@ -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, @@ -163,6 +163,10 @@ public static function getRetryFunctionByConnectionErrors( return false; } + if (empty($curlErrorCodes)) { + return true; + } + $handlerContext = $exception->getHandlerContext(); $errno = $handlerContext['errno'] ?? null; From 6319326c3c62df2a7f877a867f8ff11fba25a09c Mon Sep 17 00:00:00 2001 From: ksvirkou-hubspot Date: Mon, 11 May 2026 16:52:10 +0300 Subject: [PATCH 2/3] Fix test calls after getRetryFunctionByConnectionErrors param reorder Co-Authored-By: Claude Sonnet 4.6 (1M context) --- tests/Unit/RetryMiddlewareFactoryTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Unit/RetryMiddlewareFactoryTest.php b/tests/Unit/RetryMiddlewareFactoryTest.php index a4d249dc9..92b137793 100644 --- a/tests/Unit/RetryMiddlewareFactoryTest.php +++ b/tests/Unit/RetryMiddlewareFactoryTest.php @@ -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', @@ -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', @@ -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', @@ -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', From e18da96934dd6663e5deab102b2898e50db46ea0 Mon Sep 17 00:00:00 2001 From: ksvirkou-hubspot Date: Mon, 11 May 2026 16:55:02 +0300 Subject: [PATCH 3/3] CS fix --- lib/Delay.php | 2 +- lib/RetryMiddlewareFactory.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Delay.php b/lib/Delay.php index 12f05dd7e..25e1a2ed5 100644 --- a/lib/Delay.php +++ b/lib/Delay.php @@ -19,7 +19,7 @@ public static function getLinearDelayFunction() } /** - * @deprecated Pass null as the delay function instead — Guzzle will apply its built-in exponential delay via \GuzzleHttp\RetryMiddleware::exponentialDelay. + * @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) { diff --git a/lib/RetryMiddlewareFactory.php b/lib/RetryMiddlewareFactory.php index 81f2ba172..45ccc0e43 100644 --- a/lib/RetryMiddlewareFactory.php +++ b/lib/RetryMiddlewareFactory.php @@ -174,7 +174,7 @@ public static function getRetryFunctionByConnectionErrors( 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); }