mingalevme/retryable-psr-http-client

The package provides a familiar PSR HTTP Client interface with automatic retries and exponential backoff (and others strategies). It is a thin wrapper over the standard PSR HTTP Client and exposes same public API. This makes the decorator very easy to drop into existing programs.

2.0.0 2024-07-05 10:42 UTC

This package is auto-updated.

Last update: 2024-10-05 11:22:29 UTC


README

quality codecov version license

Simple Retryable Psr Http Client Decorator with Retry-After-header support* and 100% code coverage.

NOTE: Retry-After-header handling is disabled by default because relying on untrusted headers it risky and dangerous, turn it on only if you clearly understand the consequences.

Composer

composer require mingalevme/retryable-psr-http-client

Example 1 (Simple drop-in replacement)

  • Max 3 attempts.
  • Triggers on 5xx/429 response status codes and/or Psr\Http\Client\ClientExceptionInterface.
  • Exponential backoff: 2 power of attempt number (1, 2, 4, 8, ...).
use Mingalevme\RetryablePsrHttpClient\RetryablePsrHttpClient;
use Psr\Http\Client\ClientInterface;

$someDiContainer->decorate(ClientInterface::class, function (ClientInterface $client): RetryablePsrHttpClient {
    return new RetryablePsrHttpClient($client);
});

Example 2 (Extended Usage)

  • Max 5 attempts.
  • Respect Retry-After-header.
  • Liner backoff with 1s (initial value) + 2s (slope).
  • Triggers on 4xx, 5xx and Psr\Http\Client\ClientExceptionInterface.
  • Log on any error (unacceptable response or Psr\Http\Client\ClientExceptionInterface-exception).
<?php

use Mingalevme\RetryablePsrHttpClient\BackoffCalc\LinearBackoffCalc;
use Mingalevme\RetryablePsrHttpClient\Config;
use Mingalevme\RetryablePsrHttpClient\NullEventListener;
use Mingalevme\RetryablePsrHttpClient\ResponseAnalyzer\ResponseAnalyzerInterface;
use Mingalevme\RetryablePsrHttpClient\RetryablePsrHttpClient;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;

final class MyAppHttpClientErrLogEventListener extends AbstractEventListener
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    public function onError(
        int $attemptNumber,
        RequestInterface $request,
        ClientExceptionInterface|ResponseInterface $error,
    ): void {
        $this->logger->error("Error while sending request {$request->getUri()}, attempt #$attemptNumber");
    }
}

final class MyAppHttpResponseAnalyzer implements ResponseAnalyzerInterface
{
    public function isAcceptable(ResponseInterface $response): bool
    {
        return $response->getStatusCode() >= 400;
    }
}

$someDiContainer->decorate(
    ClientInterface::class,
    function (
        ClientInterface $client,
        MyAppHttpClientErrLogEventListener $listener,
        MyAppHttpResponseAnalyzer $responseAnalyzer,
    ): RetryablePsrHttpClient {
        $config = Config::new()
            ->setRetryCount(5)
            ->setRespectRetryAfterHeader(true)
            ->setBackoffCalc(new LinearBackoffCalc(2, 1))
            ->setResponseAnalyzer($responseAnalyzer)
            ->addEventListener($listener);
        return new RetryablePsrHttpClient($client, $config);
    },
);