caseyamcl/guzzle_retry_middleware

Guzzle v6+ 重试中间件,处理 429/503 状态码和连接超时

v2.11.0 2024-09-16 19:01 UTC

This package is auto-updated.

Last update: 2024-09-16 19:04:11 UTC


README

Latest Version on Packagist Software License Github Build Code coverage PHPStan Level 8 Total Downloads

这是一个 Guzzle v6/7+ 中间件库,当 HTTP 服务器响应 503429 状态码时,它将自动重试请求。它还可以配置重试超时的请求。

如果服务器提供了一个 Retry-After 头,这个中间件将根据服务器指示的等待时间延迟后续请求。

与内置的 RetryAfter 中间件不同,这个中间件提供了一些基于 HTTP 规范中规则的默认重试行为。您可以直接将其添加到请求堆栈中,而无需任何额外的配置。

功能,速览

  • 当服务器响应 429 或 503 状态码(或任何 HTTP 状态码,这是可配置的)时,自动重试 HTTP 请求
  • 根据 Retry-After HTTP 头设置重试延迟,如果发送了,或者如果没有发送 Retry-After 头,则自动指数退避(也可配置)
  • 可选地重试超时的请求(通过 connect_timeouttimeout 选项)
  • 在发生重试时设置可选的回调(用于记录/报告)
  • 指定在放弃之前最大重试次数(默认:10)
  • 近 100% 的测试覆盖率,良好的内联文档,PSR-12 合规

安装

通过 Composer

$ composer require caseyamcl/guzzle_retry_middleware

用法

基本上

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleRetry\GuzzleRetryMiddleware;

$stack = HandlerStack::create();
$stack->push(GuzzleRetryMiddleware::factory());

$client = new Client(['handler' => $stack]);

// Make requests galore...

这是默认配置。如果 HTTP 服务器响应 429503 状态码,此中间件将拦截响应并重试,最多重试 10 次,然后放弃并执行 Guzzle 默认行为(默认情况下,抛出 BadResponseException)。

如果服务器提供了 RetryAfter 头,此中间件将在尝试请求之前等待指定的时间。如果没有,则每次请求之间的等待时间将越来越长,直到 10 次尝试后放弃。

选项

以下选项可用

以下将详细介绍每个选项。

配置选项

选项可以在三个位置之一设置

// Per request, in the same array as other Guzzle options
$response = $client->get('/some-url', [
   'max_retry_attempts' => 5,
   'on_retry_callback'  => $notifier
]);

// When you instantiate Guzzle, in the same array as other Guzzle options
$client = new \GuzzleHttp\Client([

    // Standard Guzzle options
    'base_url'        => 'https://example.org',
    'connect_timeout' => 10.0,
    
    // Retry options
    'max_retry_attempts' => 5,
    'on_retry_callback'  => $notifier
]);

// When you instantiate the Retry middleware
$stack = \GuzzleHttp\Stack::create();
$stack->push(GuzzleRetryMiddleware::factory([
    'max_retry_attempts' => 5,
    'on_retry_callback'  => $notifier
]));

如果您在两个或多个位置指定了选项,则配置将按以下方式合并

  1. 单个请求选项优先于 Guzzle 构造函数选项
  2. Guzzle 构造函数选项优先于中间件构造函数选项。

设置最大重试次数

此值应是一个大于等于 0 的整数。设置 0 或负数将有效地禁用此中间件。

将此值设置为 0 有用,当您想默认重试尝试,但为特定请求禁用重试时

// Set the default retry attempts to 5
$client = new \GuzzleHttp\Client(['max_retry_attempts' => 5]);

// Do not retry this request
$client->get('/some/url', ['max_retry_attempts' => 0]);

设置要重试的状态码

默认情况下,此中间件将在服务器响应 429503 HTTP 状态码时重试请求。但是,您可以配置此

$response = $client->get('/some-path', [
    'retry_on_status' => [429, 503, 500]
]);

如果响应包括 RetryAfter 头,但其状态码不在列表中,则不会进行处理。

注意: 我还没有测试这个,但我真诚地相信如果你尝试使用这个中间件与3xx响应一起使用,你可能会看到一些古怪的行为。我不建议这么做。

设置默认重试延迟

如果响应中包含有效的RetryAfter头,这个中间件将根据服务器在该头中指定的时长延迟下一次重试尝试。

如果响应中包含一个非有效RetryAfter或没有提供RetryAfter头,那么这个中间件将使用默认的退避算法:multipler * number-of-attempts

带有RetryAfter头的响应

      Client                 Server
      ------                 ------
      GET /resource    -> 
                       <-    429 Response with `Retry-After: 120`
      wait 120s                 
      GET /resource    ->   
                       <-    200 OK

没有RetryAfter,请求次数乘以乘数(默认:1.5

      Client                 Server
      ------                 ------
      GET /resource    -> 
                       <-    429 Response (no Retry-After header)
      wait 1.5 x 1s                 
      GET /resource    ->   
                       <-    429 Response (no Retry-After header)
      wait 1.5 x 2s                 
      GET /resource    ->   
                       <-    429 Response (no Retry-After header)
      wait 1.5 x 3s                 
      GET /resource    ->   
                       <-    429 Response (no Retry-After header)
      wait 1.5 x 4s                 
      GET /resource    ->   
                       <-    200 OK

你可以设置自定义的默认乘数

$response = $client->get('/some-path', [
    'default_retry_multiplier' => 2.5
]);

你也可以通过指定default_retry_multiplier的调用对象来传递设置默认延迟超时的自定义算法

// Custom callback to determine default timeout.  Note: $response may be NULL if a connect timeout occurred.
$response = $client->get('/some-path', [
    'default_retry_multiplier' => function($numRequests, ?ResponseInterface $response): float {
        return (float) rand(1, 5);       
    }
]);

重试超时请求

你可以配置这个中间件来重试超时请求。只需将retry_on_timeout选项设置为true

// Retry this request if it times out:
$response = $client->get('/some-path', [
    'retry_on_timeout' => true,    // Set the retry middleware to retry when the connection or response times out
    'connect_timeout'  => 20,     // This is a built-in Guzzle option
    'timeout'          => 50      // This is also a built-in Guzzle option
]);

// You can also set these as defaults for every request:
$guzzle = new \GuzzleHttp\Client(['retry_on_timeout' => true, 'connect_timeout' => 20]);
$response = $guzzle->get('https://example.org');

重试回调

你可以提供一个回调方法,在每次请求重试之前将被调用。这对于记录、报告或其他任何你可以想到的事情都很有用。

如果你指定了回调,它将在中间件调用usleep()延迟函数之前被调用。

requestoptions参数通过引用传递,以便你在回调中修改它们,在请求重新发送之前。

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * Listen for retry events
 *
 * @param int                    $attemptNumber  How many attempts have been tried for this particular request
 * @param float                  $delay          How long the client will wait before retrying the request
 * @param RequestInterface       $request        Request
 * @param array                  $options        Guzzle request options
 * @param ResponseInterface|null $response       Response (or NULL if response not sent; e.g. connect timeout)
 * @param Throwable|null         $exception      This value will be present if the retry was triggered by onRejected
 *                                               (e.g. in the event of a connection timeout)                                                
 */
$listener = function(
    int $attemptNumber,
    float $delay,
    RequestInterface &$request,
    array &$options,
    ?ResponseInterface $response,
    ?Throwable $exception
) {
    
    echo sprintf(
        "Retrying request to %s.  Server responded with %s.  Will wait %s seconds.  This is attempt #%s. The error was %s",
        $request->getUri()->getPath(),
        $response->getStatusCode(),
        number_format($delay, 2),
        $attemptNumber,
        $exception->getMessage()
    );
}

$client = new \GuzzleHttp\Client([
    'on_retry_callback' => $listener
]);

$response = $client->get('/some/path');

按请求启用或禁用

假设你已经按照以下方式设置了默认重试选项

$stack = \GuzzleHttp\Stack::create();
$stack->push(GuzzleRetryMiddleware::factory(['max_retry_attempts' => 5]));
$client = new \GuzzleHttp\Client(['handler' => $stack]);

你可以通过设置请求选项中的retry_enabled参数来禁用单个请求的重试

// Retry will NOT be attempted for this request..
$client->get('https://example.org', ['retry_enabled' => false]);

// Retry WILL be attempted for this request...
$client->get('https://example.org');

向HTTP响应中添加自定义重试头

有时为了调试目的,知道在获取响应时请求重试了多少次是有用的。为此目的,这个库可以向响应添加自定义头;只需将expose_retry_header选项设置为TRUE

注意:这修改了客户端的HTTP响应。如果你不想修改从服务器检索的响应,你也可以使用回调来获取请求计数。

示例

// Retry this request if it times out:
$response = $client->get('/some-path', [
    'expose_retry_header' => true  // This adds the 'X-Retry-Counter' if a request was retried
]);

// If a request was retried, the response will include the 'X-Retry-Counter'
$numRetries = (int) $response->getHeaderLine('X-Retry-Counter');

你也可以指定自定义头键

// Retry this request if it times out:
$response = $client->get('/some-path', [
    'expose_retry_header' => true,
    'retry_header'        => 'X-Retry-Count'
]);

// If a request was retried, the response will include the 'X-Retry-Counter'
$numRetries = (int) $response->getHeaderLine('X-Retry-Count');

修改期望的头名称从Retry-After

你可以更改客户端期望服务器响应的头。默认情况下,客户端查找Retry-After头,但在某些边缘情况下,服务器可能选择响应不同的头。

// Change the name of the expected retry after header to something else:
$response = $client->get('/some-path', [
    'retry_after_header' => 'X-Custom-Retry-After-Seconds'
]);

// Otherwise, the default `Retry-After` header will be used.
$response = $client->get('/some-path');

RetryAfter头设置自定义日期格式

你可以更改客户端库期望的服务器期望的日期格式。默认情况下,此库期望RFC 2822头,如HTTP规范中定义。在某些边缘情况下,服务器可能实现其他日期格式。此库允许这种情况。

注意:请小心不要使用Unix纪元(u)格式。客户端将此值解释为整数,并随后超时
非常、非常长时间。

设置最大允许的超时值

如果你想让客户端不接受超过特定值的超时值,请设置max_allowable_timeout_secs选项。这将在超时达到指定长度时返回一个静态数字,无论它是使用默认退避算法计算的还是从服务器通过RetryAfter头返回。

默认情况下,此值是 null,表示没有限制。

// Set the maximum allowable timeout
// If the calculated value exceeds 120 seconds, then just return 120 seconds
$response = $client->get('/some-path', [
    'max_allowable_timeout_secs' => 120
]);

为所有重试设置时间上限

如果您想为所有重试请求设置时间上限,请设置 give_up_after_secs 选项。如果设置了该选项,则会先检查时间上限,即使未达到重试次数限制,任何请求也会失败。

// This will fail when either the number of seconds is reached, or the number of retry attempts is reached, whichever
// comes first 
$response = $client->get('/some-path', [
    'max_retry_attempts' => 10 
    'give_up_after_secs' => 10
]);

设置特定HTTP方法进行重试

默认情况下,此库重试所有请求方法(GETPOSTPATCH 等)。如果您想限制此库将重试的HTTP方法,请使用方法数组指定 retry_on_methods 选项。

//
$response = $client->get('/some-path', [
    'retry_on_methods' => ['GET', 'OPTIONS', 'HEAD']
]);

自定义重试决策逻辑

有时,服务器在需要重试响应时可能会未能提供适当的HTTP错误代码。例如,考虑一个返回 200 状态代码,但消息体指示您等待的服务器。在这种情况下,您可以使用 should_retry_callback 选项来实现一个回调方法,该方法返回 true(应该重试)或 false(不应该重试)。

此回调的另一种用途可以基于请求本身。例如,您可能只想重试设置了特定头部的请求。

请注意,只有当以下所有情况都成立时,回调才会被调用

  • retry_enabled 选项为 true(默认:true
  • 重试次数未超过在 max_retry_attempts 中设置的值(默认:10
  • 总经过时间小于 give_up_after_secs 值(默认:禁用
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;


$callback = function (array $options, ?ResponseInterface $response, RequestInterface $request): bool {
    // Only allow retries if x-header header is set
    if(! $request->hasHeader('x-header')) {
        return false; 
    }

    // Response will be NULL in the event of a connection timeout, so your callback function
    // will need to be able to handle that case
    if (! $response) {
        return true;
    }

    // Get the HTTP body as a string
    $body = (string) $response->getBody();
    return str_contains($body, 'error'); // NOTE: The str_contains function is available only in PHP 8+ 
};

$response = $client->get('/some-path', [
    // ..other options..,
   'should_retry_callback' => $callback
]);

变更日志

有关最近更改的更多信息,请参阅 CHANGELOG

测试

$ composer test

注意: 由于此库测试超时,一些测试可能需要 2-3 秒才能运行。

贡献

有关详细信息,请参阅 CONTRIBUTING

安全

如果您发现任何安全相关的问题,请通过电子邮件 [email protected] 而不是使用问题跟踪器。

鸣谢

许可协议

MIT 许可协议(MIT)。有关更多信息,请参阅 许可文件