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