Skip to content

Instantly share code, notes, and snippets.

@devster
Created January 16, 2020 11:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devster/a1823eac5cd721fa3b9b25f04157a344 to your computer and use it in GitHub Desktop.
Save devster/a1823eac5cd721fa3b9b25f04157a344 to your computer and use it in GitHub Desktop.
Retry decorator for symfony HttpClientInterface
<?php
declare(strict_types=1);
namespace App\HttpClient;
use App\HttpClient\Response\RetryResponse;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
final class RetryHttpClient implements HttpClientInterface
{
private HttpClientInterface $client;
private LoggerInterface $logger;
public function __construct(HttpClientInterface $client, LoggerInterface $logger)
{
$this->client = $client;
$this->logger = $logger;
}
/**
* @param array<mixed> $options
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$retries = $options['retries'] ?? 0;
unset($options['retries']);
return new RetryResponse(
$this->client->request($method, $url, $options),
$this->client,
$this->logger,
[$method, $url, $options],
$retries
);
}
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
}
<?php
declare(strict_types=1);
namespace App\HttpClient\Response;
use App\Exception\RetryTransportException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
final class RetryResponse implements ResponseInterface
{
private ResponseInterface $response;
private HttpClientInterface $client;
private LoggerInterface $logger;
/**
* $request = [$method, $url, $options]
*
* @var array<mixed>
*/
private array $request;
private int $retries;
/**
* @param array<mixed> $request
*/
public function __construct(
ResponseInterface $response,
HttpClientInterface $client,
LoggerInterface $logger,
array $request,
int $retries
) {
$this->response = $response;
$this->client = $client;
$this->logger = $logger;
$this->request = $request;
$this->retries = $retries;
}
/**
* @return mixed
*/
private function retry(\Closure $closure)
{
$retries = $this->retries + 1;
while ($retries) {
$retries--;
try {
return $closure();
} catch (TransportExceptionInterface $e) {
if (0 === $retries) {
throw new RetryTransportException($e, $this->retries);
}
$this->logger->notice('TransportException: {message}. Handled by retry. Remaining retries: {remaining}', [
'message' => $e->getMessage(),
'remaining' => $retries,
'exception' => $e,
]);
sleep(1);
$this->response = $this->client->request(...$this->request);
}
}
return;
}
public function __destruct()
{
$this->retry(function (): void {
if (!method_exists($this->response, '__destruct')) {
return;
}
$this->response->__destruct();
});
}
public function getStatusCode(): int
{
return $this->retry(function (): int {
return $this->response->getStatusCode();
});
}
public function getHeaders(bool $throw = true): array
{
return $this->retry(function () use ($throw): array {
return $this->response->getHeaders($throw);
});
}
public function getContent(bool $throw = true): string
{
return $this->retry(function () use ($throw): string {
return $this->response->getContent($throw);
});
}
/**
* @return array<mixed>
*/
public function toArray(bool $throw = true): array
{
return $this->retry(function () use ($throw): array {
return $this->response->toArray($throw);
});
}
public function cancel(): void
{
$this->response->cancel();
}
public function getInfo(string $type = null)
{
return $this->response->getInfo($type);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment