1 | <?php |
||
43 | class GuzzleRetryMiddleware |
||
44 | { |
||
45 | // HTTP date format |
||
46 | const DATE_FORMAT = 'D, d M Y H:i:s T'; |
||
47 | |||
48 | // Default retry header (off by default; configurable) |
||
49 | const RETRY_HEADER = 'X-Retry-Counter'; |
||
50 | |||
51 | /** |
||
52 | * @var array |
||
53 | */ |
||
54 | private $defaultOptions = [ |
||
55 | |||
56 | // Retry enabled. Toggle retry on or off per request |
||
57 | 'retry_enabled' => true, |
||
58 | |||
59 | // If server doesn't provide a Retry-After header, then set a default back-off delay |
||
60 | // NOTE: This can either be a float, or it can be a callable that returns a (accepts count and response|null) |
||
61 | 'default_retry_multiplier' => 1.5, |
||
62 | |||
63 | // Set a maximum number of attempts per request |
||
64 | 'max_retry_attempts' => 10, |
||
65 | |||
66 | // Set this to TRUE to retry only if the HTTP Retry-After header is specified |
||
67 | 'retry_only_if_retry_after_header' => false, |
||
68 | |||
69 | // Only retry when status is equal to these response codes |
||
70 | 'retry_on_status' => ['429', '503'], |
||
71 | |||
72 | // Callback to trigger when delay occurs (accepts count, delay, request, response, options) |
||
73 | 'on_retry_callback' => null, |
||
74 | |||
75 | // Retry on connect timeout? |
||
76 | 'retry_on_timeout' => false, |
||
77 | |||
78 | // Add the number of retries to an X-Header |
||
79 | 'expose_retry_header' => false, |
||
80 | |||
81 | // The header key |
||
82 | 'retry_header' => self::RETRY_HEADER |
||
83 | ]; |
||
84 | |||
85 | /** |
||
86 | * @var callable |
||
87 | */ |
||
88 | private $nextHandler; |
||
89 | |||
90 | /** |
||
91 | * Provides a closure that can be pushed onto the handler stack |
||
92 | * |
||
93 | * Example: |
||
94 | * <code>$handlerStack->push(GuzzleRetryMiddleware::factory());</code> |
||
95 | * |
||
96 | * @param array $defaultOptions |
||
97 | * @return Closure |
||
98 | */ |
||
99 | 69 | public static function factory(array $defaultOptions = []): Closure |
|
105 | |||
106 | /** |
||
107 | * GuzzleRetryMiddleware constructor. |
||
108 | * |
||
109 | * @param callable $nextHandler |
||
110 | * @param array $defaultOptions |
||
111 | */ |
||
112 | 72 | public function __construct(callable $nextHandler, array $defaultOptions = []) |
|
117 | |||
118 | /** |
||
119 | * @param RequestInterface $request |
||
120 | * @param array $options |
||
121 | * @return Promise |
||
122 | */ |
||
123 | 69 | public function __invoke(RequestInterface $request, array $options): Promise |
|
141 | |||
142 | /** |
||
143 | * No exceptions were thrown during processing |
||
144 | * |
||
145 | * Depending on where this middleware is in the stack, the response could still |
||
146 | * be unsuccessful (e.g. 429 or 503), so check to see if it should be retried |
||
147 | * |
||
148 | * @param RequestInterface $request |
||
149 | * @param array $options |
||
150 | * @return callable |
||
151 | */ |
||
152 | 69 | protected function onFulfilled(RequestInterface $request, array $options): callable |
|
160 | |||
161 | /** |
||
162 | * An exception or error was thrown during processing |
||
163 | * |
||
164 | * If the reason is a BadResponseException exception, check to see if |
||
165 | * the request can be retried. Otherwise, pass it on. |
||
166 | * |
||
167 | * @param RequestInterface $request |
||
168 | * @param array $options |
||
169 | * @return callable |
||
170 | */ |
||
171 | 69 | protected function onRejected(RequestInterface $request, array $options): callable |
|
191 | |||
192 | /** |
||
193 | * @param ConnectException $e |
||
194 | * @param array $options |
||
195 | * @return bool |
||
196 | */ |
||
197 | 12 | protected function shouldRetryConnectException(ConnectException $e, array $options): bool |
|
213 | |||
214 | /** |
||
215 | * Check to see if a request can be retried |
||
216 | * |
||
217 | * This checks two things: |
||
218 | * |
||
219 | * 1. The response status code against the status codes that should be retried |
||
220 | * 2. The number of attempts made thus far for this request |
||
221 | * |
||
222 | * @param array $options |
||
223 | * @param ResponseInterface|null $response |
||
224 | * @return bool TRUE if the response should be retried, FALSE if not |
||
225 | */ |
||
226 | 57 | protected function shouldRetryHttpResponse(array $options, ResponseInterface $response): bool |
|
241 | |||
242 | /** |
||
243 | * Count the number of retries remaining. Always returns 0 or greater. |
||
244 | * @param array $options |
||
245 | * @return int |
||
246 | */ |
||
247 | 66 | protected function countRemainingRetries(array $options): int |
|
257 | |||
258 | /** |
||
259 | * Retry the request |
||
260 | * |
||
261 | * Increments the retry count, determines the delay (timeout), executes callbacks, sleeps, and re-send the request |
||
262 | * |
||
263 | * @param RequestInterface $request |
||
264 | * @param array $options |
||
265 | * @param ResponseInterface|null $response |
||
266 | * @return Promise |
||
267 | */ |
||
268 | 51 | protected function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null): Promise |
|
296 | |||
297 | /** |
||
298 | * @param array $options |
||
299 | * @param ResponseInterface $response |
||
300 | * @return ResponseInterface |
||
301 | */ |
||
302 | 57 | protected function returnResponse(array $options, ResponseInterface $response): ResponseInterface |
|
312 | |||
313 | /** |
||
314 | * Determine the delay timeout |
||
315 | * |
||
316 | * Attempts to read and interpret the HTTP `Retry-After` header, or defaults |
||
317 | * to a built-in incremental back-off algorithm. |
||
318 | * |
||
319 | * @param ResponseInterface $response |
||
320 | * @param array $options |
||
321 | * @return float Delay timeout, in seconds |
||
322 | */ |
||
323 | 51 | protected function determineDelayTimeout(array $options, ResponseInterface $response = null): float |
|
345 | |||
346 | /** |
||
347 | * Attempt to derive the timeout from the HTTP `Retry-After` header |
||
348 | * |
||
349 | * The spec allows the header value to either be a number of seconds or a datetime. |
||
350 | * |
||
351 | * @param string $headerValue |
||
352 | * @return float|null The number of seconds to wait, or NULL if unsuccessful (invalid header) |
||
353 | */ |
||
354 | 9 | protected function deriveTimeoutFromHeader(string $headerValue): ?float |
|
366 | } |
||
367 |
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: