Lines 90.32% 28 / 31
Functions and Methods 25.00% 1 / 4
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
RetryPolicy 90.32% 28 / 31 25.00% 1 / 4 13.15 0.00% 0 / 1
 decider 93.33% 14 / 15 0.00% 0 / 1 5.01
 delay 0.00% 0 / 1 0.00% 0 / 1 2
 delayMs 100.00% 6 / 6 100.00% 1 / 1 3
 retryAfterMs 88.88% 8 / 9 0.00% 0 / 1 4.02
1<?php
2
3declare(strict_types=1);
4
5namespace HushHush;
6
7use GuzzleHttp\Exception\ConnectException;
8use Psr\Http\Message\RequestInterface;
9use Psr\Http\Message\ResponseInterface;
10
11/**
12 * Retries a request only on network failure or an HTTP 5xx/429 response,
13 * using exponential backoff with jitter, and honors a `Retry-After` response
14 * header ahead of the computed backoff delay when present. Any other 4xx is
15 * never retried — it won't succeed on a second attempt, and retrying only
16 * delays the real error reaching the caller.
17 */
18final class RetryPolicy
19{
20    public static function decider(int $maxRetries): callable
21    {
22        return static function (
23            int $retries,
24            RequestInterface $request,
25            ?ResponseInterface $response = null,
26            ?\Throwable $exception = null,
27        ) use ($maxRetries): bool {
28            if ($retries >= $maxRetries) {
29                return false;
30            }
31            if ($exception instanceof ConnectException) {
32                return true;
33            }
34            if (null !== $response) {
35                $status = $response->getStatusCode();
36
37                return $status >= 500 || 429 === $status;
38            }
39
40            return false;
41        };
42    }
43
44    public static function delay(): callable
45    {
46        return self::delayMs(...);
47    }
48
49    public static function delayMs(int $retries, ?ResponseInterface $response = null): int
50    {
51        if (null !== $response) {
52            $retryAfterMs = self::retryAfterMs($response);
53            if (null !== $retryAfterMs) {
54                return $retryAfterMs;
55            }
56        }
57
58        $base = 100 * 2 ** ($retries - 1);
59
60        return (int) ($base + random_int(0, (int) $base));
61    }
62
63    private static function retryAfterMs(ResponseInterface $response): ?int
64    {
65        $retryAfter = $response->getHeaderLine('Retry-After');
66        if ('' === $retryAfter) {
67            return null;
68        }
69
70        if (is_numeric($retryAfter)) {
71            return max((int) ((float) $retryAfter * 1000), 0);
72        }
73
74        $when = strtotime($retryAfter);
75        if (false === $when) {
76            return null;
77        }
78
79        return max(($when - time()) * 1000, 0);
80    }
81}