This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | /** |
||
4 | * Guzzle Retry Middleware Library |
||
5 | * |
||
6 | * @license http://opensource.org/licenses/MIT |
||
7 | * @link https://github.com/caseyamcl/guzzle_retry_middleware |
||
8 | * @version 2.0 |
||
9 | * @package caseyamcl/guzzle_retry_middleware |
||
10 | * @author Casey McLaughlin <[email protected]> |
||
11 | * |
||
12 | * For the full copyright and license information, please view the LICENSE |
||
13 | * file that was distributed with this source code. |
||
14 | * |
||
15 | * ------------------------------------------------------------------ |
||
16 | */ |
||
17 | |||
18 | declare(strict_types=1); |
||
19 | |||
20 | namespace GuzzleRetry; |
||
21 | |||
22 | use Closure; |
||
23 | use DateTime; |
||
24 | use GuzzleHttp\Exception\BadResponseException; |
||
25 | use GuzzleHttp\Exception\ConnectException; |
||
26 | use GuzzleHttp\Promise\Promise; |
||
27 | use Psr\Http\Message\RequestInterface; |
||
28 | use Psr\Http\Message\ResponseInterface; |
||
29 | |||
30 | use function call_user_func; |
||
31 | use function call_user_func_array; |
||
32 | use function GuzzleHttp\Promise\rejection_for; |
||
33 | use function in_array; |
||
34 | use function is_callable; |
||
35 | |||
36 | /** |
||
37 | * Retry After Middleware |
||
38 | * |
||
39 | * Guzzle 6 middleware that retries requests when encountering responses |
||
40 | * with certain conditions (429 or 503). This middleware also respects |
||
41 | * the `RetryAfter` header |
||
42 | * |
||
43 | * @author Casey McLaughlin <[email protected]> |
||
44 | */ |
||
45 | class GuzzleRetryMiddleware |
||
46 | { |
||
47 | // HTTP date format |
||
48 | public const DATE_FORMAT = 'D, d M Y H:i:s T'; |
||
49 | |||
50 | // Default retry header (off by default; configurable) |
||
51 | public const RETRY_HEADER = 'X-Retry-Counter'; |
||
52 | |||
53 | // Default retry-after header |
||
54 | public const RETRY_AFTER_HEADER = 'Retry-After'; |
||
55 | |||
56 | /** |
||
57 | * @var array<mixed> |
||
58 | */ |
||
59 | private $defaultOptions = [ |
||
60 | |||
61 | // Retry enabled. Toggle retry on or off per request |
||
62 | 'retry_enabled' => true, |
||
63 | |||
64 | // If server doesn't provide a Retry-After header, then set a default back-off delay |
||
65 | // NOTE: This can either be a float, or it can be a callable that returns a (accepts count and response|null) |
||
66 | 'default_retry_multiplier' => 1.5, |
||
67 | |||
68 | // Set a maximum number of attempts per request |
||
69 | 'max_retry_attempts' => 10, |
||
70 | |||
71 | // Maximum allowable timeout seconds |
||
72 | 'max_allowable_timeout_secs' => null, |
||
73 | |||
74 | // Set this to TRUE to retry only if the HTTP Retry-After header is specified |
||
75 | 'retry_only_if_retry_after_header' => false, |
||
76 | |||
77 | // Only retry when status is equal to these response codes |
||
78 | 'retry_on_status' => ['429', '503'], |
||
79 | |||
80 | // Callback to trigger before delay occurs (accepts count, delay, request, response, options) |
||
81 | 'on_retry_callback' => null, |
||
82 | |||
83 | // Retry on connect timeout? |
||
84 | 'retry_on_timeout' => false, |
||
85 | |||
86 | // Add the number of retries to an X-Header |
||
87 | 'expose_retry_header' => false, |
||
88 | |||
89 | // The header key |
||
90 | 'retry_header' => self::RETRY_HEADER, |
||
91 | |||
92 | // The retry after header key |
||
93 | 'retry_after_header' => self::RETRY_AFTER_HEADER, |
||
94 | |||
95 | // Date format |
||
96 | 'retry_after_date_format' => self::DATE_FORMAT |
||
97 | ]; |
||
98 | |||
99 | /** |
||
100 | * @var callable |
||
101 | */ |
||
102 | private $nextHandler; |
||
103 | |||
104 | /** |
||
105 | * Provides a closure that can be pushed onto the handler stack |
||
106 | * |
||
107 | 75 | * Example: |
|
108 | * <code>$handlerStack->push(GuzzleRetryMiddleware::factory());</code> |
||
109 | * |
||
110 | 75 | * @param array<mixed> $defaultOptions |
|
111 | 75 | * @return Closure |
|
112 | */ |
||
113 | public static function factory(array $defaultOptions = []): Closure |
||
114 | { |
||
115 | return function (callable $handler) use ($defaultOptions) { |
||
116 | return new static($handler, $defaultOptions); |
||
117 | }; |
||
118 | } |
||
119 | |||
120 | 78 | /** |
|
121 | * GuzzleRetryMiddleware constructor. |
||
122 | 78 | * |
|
123 | 78 | * @param callable $nextHandler |
|
124 | 78 | * @param array<mixed> $defaultOptions |
|
125 | */ |
||
126 | final public function __construct(callable $nextHandler, array $defaultOptions = []) |
||
127 | { |
||
128 | $this->nextHandler = $nextHandler; |
||
129 | $this->defaultOptions = array_replace($this->defaultOptions, $defaultOptions); |
||
130 | } |
||
131 | 75 | ||
132 | /** |
||
133 | * @param RequestInterface $request |
||
134 | 75 | * @param array<mixed> $options |
|
135 | * @return Promise |
||
136 | */ |
||
137 | 75 | public function __invoke(RequestInterface $request, array $options): Promise |
|
138 | 75 | { |
|
139 | // Combine options with defaults specified by this middleware |
||
140 | $options = array_replace($this->defaultOptions, $options); |
||
141 | |||
142 | 75 | // Set the retry count if not already set |
|
143 | 75 | if (! isset($options['retry_count'])) { |
|
144 | 75 | $options['retry_count'] = 0; |
|
145 | 75 | } |
|
146 | 75 | ||
147 | $next = $this->nextHandler; |
||
148 | return $next($request, $options) |
||
149 | ->then( |
||
150 | $this->onFulfilled($request, $options), |
||
151 | $this->onRejected($request, $options) |
||
152 | ); |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * No exceptions were thrown during processing |
||
157 | * |
||
158 | * Depending on where this middleware is in the stack, the response could still |
||
159 | * be unsuccessful (e.g. 429 or 503), so check to see if it should be retried |
||
160 | 75 | * |
|
161 | * @param RequestInterface $request |
||
162 | * @param array<mixed> $options |
||
163 | 63 | * @return callable |
|
164 | 48 | */ |
|
165 | 63 | protected function onFulfilled(RequestInterface $request, array $options): callable |
|
166 | 75 | { |
|
167 | return function (ResponseInterface $response) use ($request, $options) { |
||
168 | return $this->shouldRetryHttpResponse($options, $response) |
||
169 | ? $this->doRetry($request, $options, $response) |
||
170 | : $this->returnResponse($options, $response); |
||
171 | }; |
||
172 | } |
||
173 | |||
174 | /** |
||
175 | * An exception or error was thrown during processing |
||
176 | * |
||
177 | * If the reason is a BadResponseException exception, check to see if |
||
178 | * the request can be retried. Otherwise, pass it on. |
||
179 | 75 | * |
|
180 | * @param RequestInterface $request |
||
181 | * @param array<mixed> $options |
||
182 | * @return callable |
||
183 | 18 | */ |
|
184 | 3 | protected function onRejected(RequestInterface $request, array $options): callable |
|
185 | 3 | { |
|
186 | return function ($reason) use ($request, $options) { |
||
187 | // If was bad response exception, test if we retry based on the response headers |
||
188 | 15 | if ($reason instanceof BadResponseException) { |
|
189 | if ($this->shouldRetryHttpResponse($options, $reason->getResponse())) { |
||
190 | 12 | return $this->doRetry($request, $options, $reason->getResponse()); |
|
191 | 6 | } |
|
192 | // If this was a connection exception, test to see if we should retry based on connect timeout rules |
||
193 | } elseif ($reason instanceof ConnectException) { |
||
194 | // If was another type of exception, test if we should retry based on timeout rules |
||
195 | if ($this->shouldRetryConnectException($options)) { |
||
196 | 12 | return $this->doRetry($request, $options); |
|
197 | 75 | } |
|
198 | } |
||
199 | |||
200 | // If made it here, then we have decided not to retry the request |
||
201 | // Future-proofing this; remove when bumping minimum Guzzle version to 7.0 |
||
202 | if (class_exists('\GuzzleHttp\Promise\Create')) { |
||
203 | return \GuzzleHttp\Promise\Create::rejectionFor($reason); |
||
204 | } else { |
||
205 | return rejection_for($reason); |
||
0 ignored issues
–
show
|
|||
206 | 12 | } |
|
207 | }; |
||
208 | 12 | } |
|
209 | 12 | ||
210 | 12 | /** |
|
211 | * Decide whether or not to retry on connect exception |
||
212 | * |
||
213 | * @param array<mixed> $options |
||
214 | * @return bool |
||
215 | */ |
||
216 | protected function shouldRetryConnectException(array $options): bool |
||
217 | { |
||
218 | return $options['retry_enabled'] |
||
219 | && ($options['retry_on_timeout'] ?? false) |
||
220 | && $this->countRemainingRetries($options) > 0; |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * Check to see if a request can be retried |
||
225 | 63 | * |
|
226 | * This checks two things: |
||
227 | 63 | * |
|
228 | * 1. The response status code against the status codes that should be retried |
||
229 | 21 | * 2. The number of attempts made thus far for this request |
|
230 | 63 | * |
|
231 | 63 | * @param array<mixed> $options |
|
232 | 63 | * @param ResponseInterface|null $response |
|
233 | 18 | * @return bool TRUE if the response should be retried, FALSE if not |
|
234 | */ |
||
235 | protected function shouldRetryHttpResponse(array $options, ?ResponseInterface $response = null): bool |
||
236 | { |
||
237 | 60 | $statuses = array_map('\intval', (array) $options['retry_on_status']); |
|
238 | $hasRetryAfterHeader = $response ? $response->hasHeader('Retry-After') : false; |
||
239 | |||
240 | switch (true) { |
||
241 | case $options['retry_enabled'] === false: |
||
242 | case $this->countRemainingRetries($options) === 0: // No Retry-After header, and it is required? Give up |
||
243 | case (! $hasRetryAfterHeader && $options['retry_only_if_retry_after_header']): |
||
244 | return false; |
||
245 | |||
246 | 66 | // Conditions met; see if status code matches one that can be retried |
|
247 | default: |
||
248 | 66 | $statusCode = $response ? $response->getStatusCode() : 0; |
|
249 | return in_array($statusCode, $statuses, true); |
||
250 | 66 | } |
|
251 | 66 | } |
|
252 | 66 | ||
253 | /** |
||
254 | 66 | * Count the number of retries remaining. Always returns 0 or greater. |
|
255 | * |
||
256 | * @param array<mixed> $options |
||
257 | * @return int |
||
258 | */ |
||
259 | protected function countRemainingRetries(array $options): int |
||
260 | { |
||
261 | $retryCount = isset($options['retry_count']) ? (int) $options['retry_count'] : 0; |
||
262 | |||
263 | $numAllowed = isset($options['max_retry_attempts']) |
||
264 | ? (int) $options['max_retry_attempts'] |
||
265 | : $this->defaultOptions['max_retry_attempts']; |
||
266 | |||
267 | 57 | return (int) max([$numAllowed - $retryCount, 0]); |
|
268 | } |
||
269 | |||
270 | 57 | /** |
|
271 | * Retry the request |
||
272 | * |
||
273 | 57 | * Increments the retry count, determines the delay (timeout), executes callbacks, sleeps, and re-send the request |
|
274 | * |
||
275 | * @param RequestInterface $request |
||
276 | 57 | * @param array<mixed> $options |
|
277 | * @param ResponseInterface|null $response |
||
278 | 48 | * @return Promise |
|
279 | */ |
||
280 | 48 | protected function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null): Promise |
|
281 | 48 | { |
|
282 | 48 | // Increment the retry count |
|
283 | 48 | ++$options['retry_count']; |
|
284 | 48 | ||
285 | // Determine the delay timeout |
||
286 | $delayTimeout = $this->determineDelayTimeout($options, $response); |
||
287 | |||
288 | // Callback? |
||
289 | if ($options['on_retry_callback']) { |
||
290 | 57 | call_user_func_array( |
|
291 | $options['on_retry_callback'], |
||
292 | [ |
||
293 | 57 | (int) $options['retry_count'], |
|
294 | (float) $delayTimeout, |
||
295 | &$request, |
||
296 | &$options, |
||
297 | $response |
||
298 | ] |
||
299 | ); |
||
300 | } |
||
301 | 63 | ||
302 | // Delay! |
||
303 | usleep((int) ($delayTimeout * 1e6)); |
||
304 | 63 | ||
305 | 63 | // Return |
|
306 | return $this($request, $options); |
||
307 | 60 | } |
|
308 | |||
309 | /** |
||
310 | 3 | * @param array<mixed> $options |
|
311 | * @param ResponseInterface $response |
||
312 | * @return ResponseInterface |
||
313 | */ |
||
314 | protected function returnResponse(array $options, ResponseInterface $response): ResponseInterface |
||
315 | { |
||
316 | if ($options['expose_retry_header'] === false || $options['retry_count'] === 0) { |
||
317 | return $response; |
||
318 | } |
||
319 | |||
320 | return $response->withHeader($options['retry_header'], $options['retry_count']); |
||
321 | } |
||
322 | |||
323 | 57 | /** |
|
324 | * Determine the delay timeout |
||
325 | 57 | * |
|
326 | * Attempts to read and interpret the configured retry after header, or defaults |
||
327 | 3 | * to a built-in incremental back-off algorithm. |
|
328 | 3 | * |
|
329 | 3 | * @param array<mixed> $options |
|
330 | * @param ResponseInterface|null $response |
||
331 | * @return float Delay timeout, in seconds |
||
332 | 54 | */ |
|
333 | protected function determineDelayTimeout(array $options, ?ResponseInterface $response = null): float |
||
334 | { |
||
335 | if (is_callable($options['default_retry_multiplier'])) { |
||
336 | $defaultDelayTimeout = (float) call_user_func( |
||
337 | 57 | $options['default_retry_multiplier'], |
|
338 | $options['retry_count'], |
||
339 | 12 | $response |
|
340 | 12 | ); |
|
341 | } else { |
||
342 | $defaultDelayTimeout = (float) $options['default_retry_multiplier'] * $options['retry_count']; |
||
343 | 45 | } |
|
344 | |||
345 | // Retry-After can be a delay in seconds or a date |
||
346 | // (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) |
||
347 | if ($response && $response->hasHeader($options['retry_after_header'])) { |
||
348 | $timeout = $this->deriveTimeoutFromHeader( |
||
349 | $response->getHeader($options['retry_after_header'])[0], |
||
350 | $options['retry_after_date_format'] |
||
351 | ) ?: $defaultDelayTimeout; |
||
352 | } else { |
||
353 | $timeout = abs($defaultDelayTimeout); |
||
354 | 12 | } |
|
355 | |||
356 | // If the max_allowable_timeout_secs is set |
||
357 | if (! is_null($options['max_allowable_timeout_secs']) && abs($options['max_allowable_timeout_secs']) > 0) { |
||
358 | 12 | return min(abs($timeout), (float) abs($options['max_allowable_timeout_secs'])); |
|
359 | 6 | } else { |
|
360 | 6 | return abs($timeout); |
|
361 | 3 | } |
|
362 | } |
||
363 | |||
364 | 3 | /** |
|
365 | * Attempt to derive the timeout from the `Retry-After` (or custom) header value |
||
366 | * |
||
367 | * The spec allows the header value to either be a number of seconds or a datetime. |
||
368 | * |
||
369 | * @param string $headerValue |
||
370 | * @param string $dateFormat |
||
371 | * @return float|null The number of seconds to wait, or NULL if unsuccessful (invalid header) |
||
372 | */ |
||
373 | protected function deriveTimeoutFromHeader(string $headerValue, string $dateFormat = self::DATE_FORMAT): ?float |
||
374 | { |
||
375 | // The timeout will either be a number or a HTTP-formatted date, |
||
376 | // or seconds (integer) |
||
377 | if (is_numeric($headerValue)) { |
||
378 | return (float) trim($headerValue); |
||
379 | } elseif ($date = DateTime::createFromFormat($dateFormat ?: self::DATE_FORMAT, trim($headerValue))) { |
||
380 | return (float) $date->format('U') - time(); |
||
381 | } |
||
382 | |||
383 | return null; |
||
384 | } |
||
385 | } |
||
386 |
This function has been deprecated. The supplier of the file has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.