Skip to content

Commit 7a14c0a

Browse files
Merge pull request #614 from AScriver/master
Add retry middleware support for transient cURL connection errors (52/55/56)
2 parents c0fabd7 + 49eaa58 commit 7a14c0a

3 files changed

Lines changed: 126 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ $handlerStack->push(
7474
)
7575
);
7676

77+
$handlerStack->push(
78+
\HubSpot\RetryMiddlewareFactory::createConnectionErrorsMiddleware(
79+
\HubSpot\Delay::getExponentialDelayFunction(2)
80+
)
81+
);
82+
7783
$client = new \GuzzleHttp\Client(['handler' => $handlerStack]);
7884

7985
$hubspot = \HubSpot\Factory::createWithAccessToken('your-access-token', $client);

lib/RetryMiddlewareFactory.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,31 @@
22

33
namespace HubSpot;
44

5+
use GuzzleHttp\Exception\ConnectException;
56
use GuzzleHttp\Middleware;
67
use GuzzleHttp\Psr7\Request;
78
use GuzzleHttp\Psr7\Response;
89

910
class RetryMiddlewareFactory
1011
{
1112
public const DEFAULT_MAX_RETRIES = 5;
13+
public const TRANSIENT_CURL_ERROR_CODES = [52, 55, 56];
1214
public const INTERNAL_ERROR_RANGES = [
1315
['from' => 500, 'to' => 503],
1416
['from' => 520, 'to' => 599],
1517
];
1618

19+
public static function createConnectionErrorsMiddleware(
20+
?callable $delayFunction = null,
21+
int $maxRetries = self::DEFAULT_MAX_RETRIES,
22+
array $curlErrorCodes = self::TRANSIENT_CURL_ERROR_CODES
23+
): callable {
24+
return Middleware::retry(
25+
static::getRetryFunctionByConnectionErrors($maxRetries, $curlErrorCodes),
26+
$delayFunction
27+
);
28+
}
29+
1730
public static function createInternalErrorsMiddleware(
1831
?callable $delayFunction = null,
1932
int $maxRetries = self::DEFAULT_MAX_RETRIES
@@ -131,4 +144,37 @@ public static function getRetryFunction(
131144
return false;
132145
};
133146
}
147+
148+
public static function getRetryFunctionByConnectionErrors(
149+
int $maxRetries = self::DEFAULT_MAX_RETRIES,
150+
array $curlErrorCodes = self::TRANSIENT_CURL_ERROR_CODES
151+
): callable {
152+
return function (
153+
$retries,
154+
Request $request,
155+
?Response $response = null,
156+
$exception = null
157+
) use ($maxRetries, $curlErrorCodes) {
158+
if ($retries >= $maxRetries) {
159+
return false;
160+
}
161+
162+
if (!$exception instanceof ConnectException) {
163+
return false;
164+
}
165+
166+
$handlerContext = $exception->getHandlerContext();
167+
$errno = $handlerContext['errno'] ?? null;
168+
169+
if (is_numeric($errno) && in_array((int) $errno, $curlErrorCodes, true)) {
170+
return true;
171+
}
172+
173+
if (preg_match('/cURL error\s+(\d+):/i', $exception->getMessage(), $matches) === 1) {
174+
return in_array((int) $matches[1], $curlErrorCodes, true);
175+
}
176+
177+
return false;
178+
};
179+
}
134180
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace Hubspot\Tests\Unit;
4+
5+
use GuzzleHttp\Exception\ConnectException;
6+
use GuzzleHttp\Psr7\Request;
7+
use HubSpot\RetryMiddlewareFactory;
8+
use PHPUnit\Framework\TestCase;
9+
10+
/**
11+
* @internal
12+
*
13+
* @coversNothing
14+
*/
15+
class RetryMiddlewareFactoryTest extends TestCase
16+
{
17+
/** @test */
18+
public function itRetriesRetriableConnectionErrorsByErrno(): void
19+
{
20+
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(3);
21+
$request = new Request('GET', 'https://api.hubapi.com/test');
22+
$exception = new ConnectException(
23+
'cURL error 56: OpenSSL SSL_read unexpected eof while reading',
24+
$request,
25+
null,
26+
['errno' => 56]
27+
);
28+
29+
$this->assertTrue($retry(0, $request, null, $exception));
30+
}
31+
32+
/** @test */
33+
public function itRetriesRetriableConnectionErrorsByMessageWhenErrnoMissing(): void
34+
{
35+
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(3);
36+
$request = new Request('GET', 'https://api.hubapi.com/test');
37+
$exception = new ConnectException(
38+
'cURL error 55: Send failure: Broken pipe',
39+
$request
40+
);
41+
42+
$this->assertTrue($retry(0, $request, null, $exception));
43+
}
44+
45+
/** @test */
46+
public function itDoesNotRetryNonRetriableConnectionErrors(): void
47+
{
48+
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(3);
49+
$request = new Request('GET', 'https://api.hubapi.com/test');
50+
$exception = new ConnectException(
51+
'cURL error 60: SSL certificate problem',
52+
$request,
53+
null,
54+
['errno' => 60]
55+
);
56+
57+
$this->assertFalse($retry(0, $request, null, $exception));
58+
}
59+
60+
/** @test */
61+
public function itStopsRetryingWhenMaxRetriesReached(): void
62+
{
63+
$retry = RetryMiddlewareFactory::getRetryFunctionByConnectionErrors(1);
64+
$request = new Request('GET', 'https://api.hubapi.com/test');
65+
$exception = new ConnectException(
66+
'cURL error 56: OpenSSL SSL_read unexpected eof while reading',
67+
$request,
68+
null,
69+
['errno' => 56]
70+
);
71+
72+
$this->assertFalse($retry(1, $request, null, $exception));
73+
}
74+
}

0 commit comments

Comments
 (0)