[HttpClient] Added RetryHttpClient
This commit is contained in:
parent
f1f37a899c
commit
712ac5999d
@ -10,6 +10,7 @@ CHANGELOG
|
|||||||
`cache_clearer`, `filesystem` and `validator` services to private.
|
`cache_clearer`, `filesystem` and `validator` services to private.
|
||||||
* Added `TemplateAwareDataCollectorInterface` and `AbstractDataCollector` to simplify custom data collector creation and leverage autoconfiguration
|
* Added `TemplateAwareDataCollectorInterface` and `AbstractDataCollector` to simplify custom data collector creation and leverage autoconfiguration
|
||||||
* Add `cache.adapter.redis_tag_aware` tag to use `RedisCacheAwareAdapter`
|
* Add `cache.adapter.redis_tag_aware` tag to use `RedisCacheAwareAdapter`
|
||||||
|
* added `framework.http_client.retry_failing` configuration tree
|
||||||
|
|
||||||
5.1.0
|
5.1.0
|
||||||
-----
|
-----
|
||||||
|
@ -17,6 +17,7 @@ use Doctrine\DBAL\Connection;
|
|||||||
use Symfony\Bundle\FullStack;
|
use Symfony\Bundle\FullStack;
|
||||||
use Symfony\Component\Asset\Package;
|
use Symfony\Component\Asset\Package;
|
||||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||||
|
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
|
||||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||||
@ -1367,6 +1368,25 @@ class Configuration implements ConfigurationInterface
|
|||||||
->info('HTTP Client configuration')
|
->info('HTTP Client configuration')
|
||||||
->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}()
|
->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}()
|
||||||
->fixXmlConfig('scoped_client')
|
->fixXmlConfig('scoped_client')
|
||||||
|
->beforeNormalization()
|
||||||
|
->always(function ($config) {
|
||||||
|
if (empty($config['scoped_clients']) || !\is_array($config['default_options']['retry_failed'] ?? null)) {
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($config['scoped_clients'] as &$scopedConfig) {
|
||||||
|
if (!isset($scopedConfig['retry_failed']) || true === $scopedConfig['retry_failed']) {
|
||||||
|
$scopedConfig['retry_failed'] = $config['default_options']['retry_failed'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (\is_array($scopedConfig['retry_failed'])) {
|
||||||
|
$scopedConfig['retry_failed'] = $scopedConfig['retry_failed'] + $config['default_options']['retry_failed'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
})
|
||||||
|
->end()
|
||||||
->children()
|
->children()
|
||||||
->integerNode('max_host_connections')
|
->integerNode('max_host_connections')
|
||||||
->info('The maximum number of connections to a single host.')
|
->info('The maximum number of connections to a single host.')
|
||||||
@ -1452,6 +1472,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->variableNode('md5')->end()
|
->variableNode('md5')->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
->append($this->addHttpClientRetrySection())
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->scalarNode('mock_response_factory')
|
->scalarNode('mock_response_factory')
|
||||||
@ -1594,6 +1615,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->variableNode('md5')->end()
|
->variableNode('md5')->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
->append($this->addHttpClientRetrySection())
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
@ -1603,6 +1625,50 @@ class Configuration implements ConfigurationInterface
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addHttpClientRetrySection()
|
||||||
|
{
|
||||||
|
$root = new NodeBuilder();
|
||||||
|
|
||||||
|
return $root
|
||||||
|
->arrayNode('retry_failed')
|
||||||
|
->fixXmlConfig('http_code')
|
||||||
|
->canBeEnabled()
|
||||||
|
->addDefaultsIfNotSet()
|
||||||
|
->beforeNormalization()
|
||||||
|
->always(function ($v) {
|
||||||
|
if (isset($v['backoff_service']) && (isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']))) {
|
||||||
|
throw new \InvalidArgumentException('The "backoff_service" option cannot be used along with the "delay", "multiplier" or "max_delay" options.');
|
||||||
|
}
|
||||||
|
if (isset($v['decider_service']) && (isset($v['http_codes']))) {
|
||||||
|
throw new \InvalidArgumentException('The "decider_service" option cannot be used along with the "http_codes" options.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
})
|
||||||
|
->end()
|
||||||
|
->children()
|
||||||
|
->scalarNode('backoff_service')->defaultNull()->info('service id to override the retry backoff')->end()
|
||||||
|
->scalarNode('decider_service')->defaultNull()->info('service id to override the retry decider')->end()
|
||||||
|
->arrayNode('http_codes')
|
||||||
|
->performNoDeepMerging()
|
||||||
|
->beforeNormalization()
|
||||||
|
->ifArray()
|
||||||
|
->then(function ($v) {
|
||||||
|
return array_filter(array_values($v));
|
||||||
|
})
|
||||||
|
->end()
|
||||||
|
->prototype('integer')->end()
|
||||||
|
->info('A list of HTTP status code that triggers a retry')
|
||||||
|
->defaultValue([423, 425, 429, 500, 502, 503, 504, 507, 510])
|
||||||
|
->end()
|
||||||
|
->integerNode('max_retries')->defaultValue(3)->min(0)->end()
|
||||||
|
->integerNode('delay')->defaultValue(1000)->min(0)->info('Time in ms to delay (or the initial value when multiplier is used)')->end()
|
||||||
|
->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: (delay * (multiple ^ retries))')->end()
|
||||||
|
->integerNode('max_delay')->defaultValue(0)->min(0)->info('Max time in ms that a retry should ever be delayed (0 = infinite)')->end()
|
||||||
|
->end()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
private function addMailerSection(ArrayNodeDefinition $rootNode)
|
private function addMailerSection(ArrayNodeDefinition $rootNode)
|
||||||
{
|
{
|
||||||
$rootNode
|
$rootNode
|
||||||
|
@ -64,6 +64,7 @@ use Symfony\Component\Form\FormTypeExtensionInterface;
|
|||||||
use Symfony\Component\Form\FormTypeGuesserInterface;
|
use Symfony\Component\Form\FormTypeGuesserInterface;
|
||||||
use Symfony\Component\Form\FormTypeInterface;
|
use Symfony\Component\Form\FormTypeInterface;
|
||||||
use Symfony\Component\HttpClient\MockHttpClient;
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\RetryableHttpClient;
|
||||||
use Symfony\Component\HttpClient\ScopingHttpClient;
|
use Symfony\Component\HttpClient\ScopingHttpClient;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
|
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
|
||||||
@ -1979,7 +1980,10 @@ class FrameworkExtension extends Extension
|
|||||||
{
|
{
|
||||||
$loader->load('http_client.php');
|
$loader->load('http_client.php');
|
||||||
|
|
||||||
$container->getDefinition('http_client')->setArguments([$config['default_options'] ?? [], $config['max_host_connections'] ?? 6]);
|
$options = $config['default_options'] ?? [];
|
||||||
|
$retryOptions = $options['retry_failed'] ?? ['enabled' => false];
|
||||||
|
unset($options['retry_failed']);
|
||||||
|
$container->getDefinition('http_client')->setArguments([$options, $config['max_host_connections'] ?? 6]);
|
||||||
|
|
||||||
if (!$hasPsr18 = interface_exists(ClientInterface::class)) {
|
if (!$hasPsr18 = interface_exists(ClientInterface::class)) {
|
||||||
$container->removeDefinition('psr18.http_client');
|
$container->removeDefinition('psr18.http_client');
|
||||||
@ -1990,8 +1994,11 @@ class FrameworkExtension extends Extension
|
|||||||
$container->removeDefinition(HttpClient::class);
|
$container->removeDefinition(HttpClient::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
$httpClientId = $this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client';
|
if ($this->isConfigEnabled($container, $retryOptions)) {
|
||||||
|
$this->registerHttpClientRetry($retryOptions, 'http_client', $container);
|
||||||
|
}
|
||||||
|
|
||||||
|
$httpClientId = $retryOptions['enabled'] ?? false ? 'http_client.retry.inner' : ($this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client');
|
||||||
foreach ($config['scoped_clients'] as $name => $scopeConfig) {
|
foreach ($config['scoped_clients'] as $name => $scopeConfig) {
|
||||||
if ('http_client' === $name) {
|
if ('http_client' === $name) {
|
||||||
throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name));
|
throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name));
|
||||||
@ -1999,6 +2006,8 @@ class FrameworkExtension extends Extension
|
|||||||
|
|
||||||
$scope = $scopeConfig['scope'] ?? null;
|
$scope = $scopeConfig['scope'] ?? null;
|
||||||
unset($scopeConfig['scope']);
|
unset($scopeConfig['scope']);
|
||||||
|
$retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false];
|
||||||
|
unset($scopeConfig['retry_failed']);
|
||||||
|
|
||||||
if (null === $scope) {
|
if (null === $scope) {
|
||||||
$baseUri = $scopeConfig['base_uri'];
|
$baseUri = $scopeConfig['base_uri'];
|
||||||
@ -2016,6 +2025,10 @@ class FrameworkExtension extends Extension
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isConfigEnabled($container, $retryOptions)) {
|
||||||
|
$this->registerHttpClientRetry($retryOptions, $name, $container);
|
||||||
|
}
|
||||||
|
|
||||||
$container->registerAliasForArgument($name, HttpClientInterface::class);
|
$container->registerAliasForArgument($name, HttpClientInterface::class);
|
||||||
|
|
||||||
if ($hasPsr18) {
|
if ($hasPsr18) {
|
||||||
@ -2033,6 +2046,44 @@ class FrameworkExtension extends Extension
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function registerHttpClientRetry(array $retryOptions, string $name, ContainerBuilder $container)
|
||||||
|
{
|
||||||
|
if (!class_exists(RetryableHttpClient::class)) {
|
||||||
|
throw new LogicException('Retry failed request support cannot be enabled as version 5.2+ of the HTTP Client component is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $retryOptions['backoff_service']) {
|
||||||
|
$backoffReference = new Reference($retryOptions['backoff_service']);
|
||||||
|
} else {
|
||||||
|
$retryServiceId = $name.'.retry.exponential_backoff';
|
||||||
|
$retryDefinition = new ChildDefinition('http_client.retry.abstract_exponential_backoff');
|
||||||
|
$retryDefinition
|
||||||
|
->replaceArgument(0, $retryOptions['delay'])
|
||||||
|
->replaceArgument(1, $retryOptions['multiplier'])
|
||||||
|
->replaceArgument(2, $retryOptions['max_delay']);
|
||||||
|
$container->setDefinition($retryServiceId, $retryDefinition);
|
||||||
|
|
||||||
|
$backoffReference = new Reference($retryServiceId);
|
||||||
|
}
|
||||||
|
if (null !== $retryOptions['decider_service']) {
|
||||||
|
$deciderReference = new Reference($retryOptions['decider_service']);
|
||||||
|
} else {
|
||||||
|
$retryServiceId = $name.'.retry.decider';
|
||||||
|
$retryDefinition = new ChildDefinition('http_client.retry.abstract_httpstatuscode_decider');
|
||||||
|
$retryDefinition
|
||||||
|
->replaceArgument(0, $retryOptions['http_codes']);
|
||||||
|
$container->setDefinition($retryServiceId, $retryDefinition);
|
||||||
|
|
||||||
|
$deciderReference = new Reference($retryServiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$container
|
||||||
|
->register($name.'.retry', RetryableHttpClient::class)
|
||||||
|
->setDecoratedService($name)
|
||||||
|
->setArguments([new Reference($name.'.retry.inner'), $deciderReference, $backoffReference, $retryOptions['max_retries'], new Reference('logger')])
|
||||||
|
->addTag('monolog.logger', ['channel' => 'http_client']);
|
||||||
|
}
|
||||||
|
|
||||||
private function registerMailerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
|
private function registerMailerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
|
||||||
{
|
{
|
||||||
if (!class_exists(Mailer::class)) {
|
if (!class_exists(Mailer::class)) {
|
||||||
|
@ -17,6 +17,8 @@ use Psr\Http\Message\StreamFactoryInterface;
|
|||||||
use Symfony\Component\HttpClient\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\HttpClient\HttplugClient;
|
use Symfony\Component\HttpClient\HttplugClient;
|
||||||
use Symfony\Component\HttpClient\Psr18Client;
|
use Symfony\Component\HttpClient\Psr18Client;
|
||||||
|
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
|
||||||
|
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
return static function (ContainerConfigurator $container) {
|
return static function (ContainerConfigurator $container) {
|
||||||
@ -48,5 +50,19 @@ return static function (ContainerConfigurator $container) {
|
|||||||
service(ResponseFactoryInterface::class)->ignoreOnInvalid(),
|
service(ResponseFactoryInterface::class)->ignoreOnInvalid(),
|
||||||
service(StreamFactoryInterface::class)->ignoreOnInvalid(),
|
service(StreamFactoryInterface::class)->ignoreOnInvalid(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// retry
|
||||||
|
->set('http_client.retry.abstract_exponential_backoff', ExponentialBackOff::class)
|
||||||
|
->abstract()
|
||||||
|
->args([
|
||||||
|
abstract_arg('delay ms'),
|
||||||
|
abstract_arg('multiplier'),
|
||||||
|
abstract_arg('max delay ms'),
|
||||||
|
])
|
||||||
|
->set('http_client.retry.abstract_httpstatuscode_decider', HttpStatusCodeDecider::class)
|
||||||
|
->abstract()
|
||||||
|
->args([
|
||||||
|
abstract_arg('http codes'),
|
||||||
|
])
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
@ -519,6 +519,7 @@
|
|||||||
<xsd:element name="resolve" type="http_resolve" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="resolve" type="http_resolve" minOccurs="0" maxOccurs="unbounded" />
|
||||||
<xsd:element name="header" type="http_header" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="header" type="http_header" minOccurs="0" maxOccurs="unbounded" />
|
||||||
<xsd:element name="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
<xsd:element name="retry-failed" type="http_client_retry_failed" minOccurs="0" maxOccurs="1" />
|
||||||
</xsd:choice>
|
</xsd:choice>
|
||||||
<xsd:attribute name="max-redirects" type="xsd:integer" />
|
<xsd:attribute name="max-redirects" type="xsd:integer" />
|
||||||
<xsd:attribute name="http-version" type="xsd:string" />
|
<xsd:attribute name="http-version" type="xsd:string" />
|
||||||
@ -535,7 +536,6 @@
|
|||||||
<xsd:attribute name="local-pk" type="xsd:string" />
|
<xsd:attribute name="local-pk" type="xsd:string" />
|
||||||
<xsd:attribute name="passphrase" type="xsd:string" />
|
<xsd:attribute name="passphrase" type="xsd:string" />
|
||||||
<xsd:attribute name="ciphers" type="xsd:string" />
|
<xsd:attribute name="ciphers" type="xsd:string" />
|
||||||
|
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
|
||||||
<xsd:complexType name="http_client_scope_options" mixed="true">
|
<xsd:complexType name="http_client_scope_options" mixed="true">
|
||||||
@ -544,6 +544,7 @@
|
|||||||
<xsd:element name="resolve" type="http_resolve" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="resolve" type="http_resolve" minOccurs="0" maxOccurs="unbounded" />
|
||||||
<xsd:element name="header" type="http_header" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="header" type="http_header" minOccurs="0" maxOccurs="unbounded" />
|
||||||
<xsd:element name="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
<xsd:element name="retry-failed" type="http_client_retry_failed" minOccurs="0" maxOccurs="1" />
|
||||||
</xsd:choice>
|
</xsd:choice>
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
<xsd:attribute name="scope" type="xsd:string" />
|
<xsd:attribute name="scope" type="xsd:string" />
|
||||||
@ -574,6 +575,20 @@
|
|||||||
</xsd:choice>
|
</xsd:choice>
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="http_client_retry_failed">
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="http-code" type="xsd:integer" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="enabled" type="xsd:boolean" />
|
||||||
|
<xsd:attribute name="backoff-service" type="xsd:string" />
|
||||||
|
<xsd:attribute name="decider-service" type="xsd:string" />
|
||||||
|
<xsd:attribute name="max-retries" type="xsd:integer" />
|
||||||
|
<xsd:attribute name="delay" type="xsd:integer" />
|
||||||
|
<xsd:attribute name="multiplier" type="xsd:float" />
|
||||||
|
<xsd:attribute name="max-delay" type="xsd:float" />
|
||||||
|
<xsd:attribute name="response_header" type="xsd:boolean" />
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
<xsd:complexType name="http_query" mixed="true">
|
<xsd:complexType name="http_query" mixed="true">
|
||||||
<xsd:attribute name="key" type="xsd:string" />
|
<xsd:attribute name="key" type="xsd:string" />
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$container->loadFromExtension('framework', [
|
||||||
|
'http_client' => [
|
||||||
|
'default_options' => [
|
||||||
|
'retry_failed' => [
|
||||||
|
'backoff_service' => null,
|
||||||
|
'decider_service' => null,
|
||||||
|
'http_codes' => [429, 500],
|
||||||
|
'max_retries' => 2,
|
||||||
|
'delay' => 100,
|
||||||
|
'multiplier' => 2,
|
||||||
|
'max_delay' => 0,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'scoped_clients' => [
|
||||||
|
'foo' => [
|
||||||
|
'base_uri' => 'http://example.com',
|
||||||
|
'retry_failed' => ['multiplier' => 4],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||||
|
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||||
|
|
||||||
|
<framework:config>
|
||||||
|
<framework:http-client>
|
||||||
|
<framework:default-options>
|
||||||
|
<framework:retry-failed
|
||||||
|
delay="100"
|
||||||
|
max-delay="0"
|
||||||
|
max-retries="2"
|
||||||
|
multiplier="2">
|
||||||
|
<framework:http-code>429</framework:http-code>
|
||||||
|
<framework:http-code>500</framework:http-code>
|
||||||
|
</framework:retry-failed>
|
||||||
|
</framework:default-options>
|
||||||
|
<framework:scoped-client name="foo" base-uri="http://example.com">
|
||||||
|
<framework:retry-failed multiplier="4"/>
|
||||||
|
</framework:scoped-client>
|
||||||
|
</framework:http-client>
|
||||||
|
</framework:config>
|
||||||
|
</container>
|
@ -0,0 +1,16 @@
|
|||||||
|
framework:
|
||||||
|
http_client:
|
||||||
|
default_options:
|
||||||
|
retry_failed:
|
||||||
|
backoff_service: null
|
||||||
|
decider_service: null
|
||||||
|
http_codes: [429, 500]
|
||||||
|
max_retries: 2
|
||||||
|
delay: 100
|
||||||
|
multiplier: 2
|
||||||
|
max_delay: 0
|
||||||
|
scoped_clients:
|
||||||
|
foo:
|
||||||
|
base_uri: http://example.com
|
||||||
|
retry_failed:
|
||||||
|
multiplier: 4
|
@ -36,11 +36,13 @@ use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionals
|
|||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Definition;
|
use Symfony\Component\DependencyInjection\Definition;
|
||||||
|
use Symfony\Component\DependencyInjection\Exception\LogicException;
|
||||||
use Symfony\Component\DependencyInjection\Loader\ClosureLoader;
|
use Symfony\Component\DependencyInjection\Loader\ClosureLoader;
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
|
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
|
||||||
use Symfony\Component\DependencyInjection\Reference;
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpClient\MockHttpClient;
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\RetryableHttpClient;
|
||||||
use Symfony\Component\HttpClient\ScopingHttpClient;
|
use Symfony\Component\HttpClient\ScopingHttpClient;
|
||||||
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
|
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
|
||||||
use Symfony\Component\Messenger\Transport\TransportFactory;
|
use Symfony\Component\Messenger\Transport\TransportFactory;
|
||||||
@ -1482,6 +1484,23 @@ abstract class FrameworkExtensionTest extends TestCase
|
|||||||
$this->assertSame($expected, $container->getDefinition('foo')->getArgument(2));
|
$this->assertSame($expected, $container->getDefinition('foo')->getArgument(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHttpClientRetry()
|
||||||
|
{
|
||||||
|
if (!class_exists(RetryableHttpClient::class)) {
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
}
|
||||||
|
$container = $this->createContainerFromFile('http_client_retry');
|
||||||
|
|
||||||
|
$this->assertSame([429, 500], $container->getDefinition('http_client.retry.decider')->getArgument(0));
|
||||||
|
$this->assertSame(100, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(0));
|
||||||
|
$this->assertSame(2, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(1));
|
||||||
|
$this->assertSame(0, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(2));
|
||||||
|
$this->assertSame(2, $container->getDefinition('http_client.retry')->getArgument(3));
|
||||||
|
|
||||||
|
$this->assertSame(RetryableHttpClient::class, $container->getDefinition('foo.retry')->getClass());
|
||||||
|
$this->assertSame(4, $container->getDefinition('foo.retry.exponential_backoff')->getArgument(1));
|
||||||
|
}
|
||||||
|
|
||||||
public function testHttpClientWithQueryParameterKey()
|
public function testHttpClientWithQueryParameterKey()
|
||||||
{
|
{
|
||||||
$container = $this->createContainerFromFile('http_client_xml_key');
|
$container = $this->createContainerFromFile('http_client_xml_key');
|
||||||
|
@ -10,6 +10,7 @@ CHANGELOG
|
|||||||
* added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
|
* added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
|
||||||
* added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
|
* added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
|
||||||
* added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
|
* added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
|
||||||
|
* added `RetryableHttpClient` to automatically retry failed HTTP requests.
|
||||||
|
|
||||||
5.1.0
|
5.1.0
|
||||||
-----
|
-----
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Retry;
|
||||||
|
|
||||||
|
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A retry backOff with a constant or exponential retry delay.
|
||||||
|
*
|
||||||
|
* For example, if $delayMilliseconds=10000 & $multiplier=1 (default),
|
||||||
|
* each retry will wait exactly 10 seconds.
|
||||||
|
*
|
||||||
|
* But if $delayMilliseconds=10000 & $multiplier=2:
|
||||||
|
* * Retry 1: 10 second delay
|
||||||
|
* * Retry 2: 20 second delay (10000 * 2 = 20000)
|
||||||
|
* * Retry 3: 40 second delay (20000 * 2 = 40000)
|
||||||
|
*
|
||||||
|
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
final class ExponentialBackOff implements RetryBackOffInterface
|
||||||
|
{
|
||||||
|
private $delayMilliseconds;
|
||||||
|
private $multiplier;
|
||||||
|
private $maxDelayMilliseconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used)
|
||||||
|
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
|
||||||
|
* @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum)
|
||||||
|
*/
|
||||||
|
public function __construct(int $delayMilliseconds = 1000, float $multiplier = 2, int $maxDelayMilliseconds = 0)
|
||||||
|
{
|
||||||
|
if ($delayMilliseconds < 0) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds));
|
||||||
|
}
|
||||||
|
$this->delayMilliseconds = $delayMilliseconds;
|
||||||
|
|
||||||
|
if ($multiplier < 1) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Multiplier must be greater than zero: "%s" given.', $multiplier));
|
||||||
|
}
|
||||||
|
$this->multiplier = $multiplier;
|
||||||
|
|
||||||
|
if ($maxDelayMilliseconds < 0) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds));
|
||||||
|
}
|
||||||
|
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDelay(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, ResponseInterface $partialResponse, \Throwable $throwable = null): int
|
||||||
|
{
|
||||||
|
$delay = $this->delayMilliseconds * $this->multiplier ** $retryCount;
|
||||||
|
|
||||||
|
if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) {
|
||||||
|
return $this->maxDelayMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $delay;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Retry;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides to retry the request when HTTP status codes belong to the given list of codes.
|
||||||
|
*
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
final class HttpStatusCodeDecider implements RetryDeciderInterface
|
||||||
|
{
|
||||||
|
private $statusCodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $statusCodes List of HTTP status codes that trigger a retry
|
||||||
|
*/
|
||||||
|
public function __construct(array $statusCodes = [423, 425, 429, 500, 502, 503, 504, 507, 510])
|
||||||
|
{
|
||||||
|
$this->statusCodes = $statusCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, ResponseInterface $partialResponse, \Throwable $throwable = null): bool
|
||||||
|
{
|
||||||
|
if ($throwable instanceof TransportExceptionInterface) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \in_array($partialResponse->getStatusCode(), $this->statusCodes, true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Retry;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
interface RetryBackOffInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the time to wait in milliseconds.
|
||||||
|
*/
|
||||||
|
public function getDelay(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, ResponseInterface $partialResponse, \Throwable $throwable = null): int;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Retry;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
interface RetryDeciderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns whether the request should be retried.
|
||||||
|
*/
|
||||||
|
public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, ResponseInterface $partialResponse, \Throwable $throwable = null): bool;
|
||||||
|
}
|
116
src/Symfony/Component/HttpClient/RetryableHttpClient.php
Normal file
116
src/Symfony/Component/HttpClient/RetryableHttpClient.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\HttpClient\Response\AsyncContext;
|
||||||
|
use Symfony\Component\HttpClient\Response\AsyncResponse;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
|
||||||
|
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
|
||||||
|
use Symfony\Component\HttpClient\Retry\RetryBackOffInterface;
|
||||||
|
use Symfony\Component\HttpClient\Retry\RetryDeciderInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically retries failing HTTP requests.
|
||||||
|
*
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
class RetryableHttpClient implements HttpClientInterface
|
||||||
|
{
|
||||||
|
use AsyncDecoratorTrait;
|
||||||
|
|
||||||
|
private $decider;
|
||||||
|
private $strategy;
|
||||||
|
private $maxRetries;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $maxRetries The maximum number of times to retry
|
||||||
|
*/
|
||||||
|
public function __construct(HttpClientInterface $client, RetryDeciderInterface $decider = null, RetryBackOffInterface $strategy = null, int $maxRetries = 3, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->decider = $decider ?? new HttpStatusCodeDecider();
|
||||||
|
$this->strategy = $strategy ?? new ExponentialBackOff();
|
||||||
|
$this->maxRetries = $maxRetries;
|
||||||
|
$this->logger = $logger ?: new NullLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||||
|
{
|
||||||
|
$retryCount = 0;
|
||||||
|
|
||||||
|
return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$retryCount) {
|
||||||
|
$exception = null;
|
||||||
|
try {
|
||||||
|
if ($chunk->isTimeout() || null !== $chunk->getInformationalStatus()) {
|
||||||
|
yield $chunk;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only retry first chunk
|
||||||
|
if (!$chunk->isFirst()) {
|
||||||
|
$context->passthru();
|
||||||
|
yield $chunk;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (TransportExceptionInterface $exception) {
|
||||||
|
// catch TransportExceptionInterface to send it to strategy.
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusCode = $context->getStatusCode();
|
||||||
|
$headers = $context->getHeaders();
|
||||||
|
if ($retryCount >= $this->maxRetries || !$this->decider->shouldRetry($method, $url, $options, $partialResponse = new MockResponse($context->getContent(), ['http_code' => $statusCode, 'headers' => $headers]), $exception)) {
|
||||||
|
$context->passthru();
|
||||||
|
yield $chunk;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context->setInfo('retry_count', $retryCount);
|
||||||
|
$context->getResponse()->cancel();
|
||||||
|
|
||||||
|
$delay = $this->getDelayFromHeader($headers) ?? $this->strategy->getDelay($retryCount, $method, $url, $options, $partialResponse, $exception);
|
||||||
|
++$retryCount;
|
||||||
|
|
||||||
|
$this->logger->info('Error returned by the server. Retrying #{retryCount} using {delay} ms delay: '.($exception ? $exception->getMessage() : 'StatusCode: '.$statusCode), [
|
||||||
|
'retryCount' => $retryCount,
|
||||||
|
'delay' => $delay,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$context->replaceRequest($method, $url, $options);
|
||||||
|
$context->pause($delay / 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDelayFromHeader(array $headers): ?int
|
||||||
|
{
|
||||||
|
if (null !== $after = $headers['retry-after'][0] ?? null) {
|
||||||
|
if (is_numeric($after)) {
|
||||||
|
return (int) $after * 1000;
|
||||||
|
}
|
||||||
|
if (false !== $time = strtotime($after)) {
|
||||||
|
return max(0, $time - time()) * 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Tests\Retry;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
|
||||||
|
|
||||||
|
class ExponentialBackOffTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDelay
|
||||||
|
*/
|
||||||
|
public function testGetDelay(int $delay, int $multiplier, int $maxDelay, int $previousRetries, int $expectedDelay)
|
||||||
|
{
|
||||||
|
$backOff = new ExponentialBackOff($delay, $multiplier, $maxDelay);
|
||||||
|
|
||||||
|
self::assertSame($expectedDelay, $backOff->getDelay($previousRetries, 'GET', 'http://example.com/', [], new MockResponse(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideDelay(): iterable
|
||||||
|
{
|
||||||
|
// delay, multiplier, maxDelay, retries, expectedDelay
|
||||||
|
yield [1000, 1, 5000, 0, 1000];
|
||||||
|
yield [1000, 1, 5000, 1, 1000];
|
||||||
|
yield [1000, 1, 5000, 2, 1000];
|
||||||
|
|
||||||
|
yield [1000, 2, 10000, 0, 1000];
|
||||||
|
yield [1000, 2, 10000, 1, 2000];
|
||||||
|
yield [1000, 2, 10000, 2, 4000];
|
||||||
|
yield [1000, 2, 10000, 3, 8000];
|
||||||
|
yield [1000, 2, 10000, 4, 10000]; // max hit
|
||||||
|
yield [1000, 2, 0, 4, 16000]; // no max
|
||||||
|
|
||||||
|
yield [1000, 3, 10000, 0, 1000];
|
||||||
|
yield [1000, 3, 10000, 1, 3000];
|
||||||
|
yield [1000, 3, 10000, 2, 9000];
|
||||||
|
|
||||||
|
yield [1000, 1, 500, 0, 500]; // max hit immediately
|
||||||
|
|
||||||
|
// never a delay
|
||||||
|
yield [0, 2, 10000, 0, 0];
|
||||||
|
yield [0, 2, 10000, 1, 0];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Tests\Retry;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
|
||||||
|
|
||||||
|
class HttpStatusCodeDeciderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testShouldRetryException()
|
||||||
|
{
|
||||||
|
$decider = new HttpStatusCodeDecider([500]);
|
||||||
|
|
||||||
|
self::assertTrue($decider->shouldRetry('GET', 'http://example.com/', [], new MockResponse(), new TransportException()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldRetryStatusCode()
|
||||||
|
{
|
||||||
|
$decider = new HttpStatusCodeDecider([500]);
|
||||||
|
|
||||||
|
self::assertTrue($decider->shouldRetry('GET', 'http://example.com/', [], new MockResponse('', ['http_code' => 500]), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsNotRetryableOk()
|
||||||
|
{
|
||||||
|
$decider = new HttpStatusCodeDecider([500]);
|
||||||
|
|
||||||
|
self::assertFalse($decider->shouldRetry('GET', 'http://example.com/', [], new MockResponse(''), null));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpClient\Exception\ServerException;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
|
||||||
|
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
|
||||||
|
use Symfony\Component\HttpClient\RetryableHttpClient;
|
||||||
|
|
||||||
|
class RetryableHttpClientTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testRetryOnError(): void
|
||||||
|
{
|
||||||
|
$client = new RetryableHttpClient(
|
||||||
|
new MockHttpClient([
|
||||||
|
new MockResponse('', ['http_code' => 500]),
|
||||||
|
new MockResponse('', ['http_code' => 200]),
|
||||||
|
]),
|
||||||
|
new HttpStatusCodeDecider([500]),
|
||||||
|
new ExponentialBackOff(0),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $client->request('GET', 'http://example.com/foo-bar');
|
||||||
|
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRetryRespectStrategy(): void
|
||||||
|
{
|
||||||
|
$client = new RetryableHttpClient(
|
||||||
|
new MockHttpClient([
|
||||||
|
new MockResponse('', ['http_code' => 500]),
|
||||||
|
new MockResponse('', ['http_code' => 500]),
|
||||||
|
new MockResponse('', ['http_code' => 200]),
|
||||||
|
]),
|
||||||
|
new HttpStatusCodeDecider([500]),
|
||||||
|
new ExponentialBackOff(0),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $client->request('GET', 'http://example.com/foo-bar');
|
||||||
|
|
||||||
|
$this->expectException(ServerException::class);
|
||||||
|
$response->getHeaders();
|
||||||
|
}
|
||||||
|
}
|
@ -48,17 +48,17 @@ class MultiplierRetryStrategy implements RetryStrategyInterface
|
|||||||
$this->maxRetries = $maxRetries;
|
$this->maxRetries = $maxRetries;
|
||||||
|
|
||||||
if ($delayMilliseconds < 0) {
|
if ($delayMilliseconds < 0) {
|
||||||
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" passed.', $delayMilliseconds));
|
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds));
|
||||||
}
|
}
|
||||||
$this->delayMilliseconds = $delayMilliseconds;
|
$this->delayMilliseconds = $delayMilliseconds;
|
||||||
|
|
||||||
if ($multiplier < 1) {
|
if ($multiplier < 1) {
|
||||||
throw new InvalidArgumentException(sprintf('Multiplier must be greater than zero: "%s" passed.', $multiplier));
|
throw new InvalidArgumentException(sprintf('Multiplier must be greater than zero: "%s" given.', $multiplier));
|
||||||
}
|
}
|
||||||
$this->multiplier = $multiplier;
|
$this->multiplier = $multiplier;
|
||||||
|
|
||||||
if ($maxDelayMilliseconds < 0) {
|
if ($maxDelayMilliseconds < 0) {
|
||||||
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" passed.', $maxDelayMilliseconds));
|
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds));
|
||||||
}
|
}
|
||||||
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
|
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user