[FrameworkBundle] Add integration of http-client component

This commit is contained in:
Thomas Talbot 2019-02-22 16:10:59 +01:00 committed by Nicolas Grekas
parent 3abf9ebc03
commit 0023a71260
19 changed files with 512 additions and 3 deletions

View File

@ -17,6 +17,7 @@ use Doctrine\DBAL\Connection;
use Symfony\Bundle\FullStack;
use Symfony\Component\Asset\Package;
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\ConfigurationInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException;
@ -109,6 +110,7 @@ class Configuration implements ConfigurationInterface
$this->addLockSection($rootNode);
$this->addMessengerSection($rootNode);
$this->addRobotsIndexSection($rootNode);
$this->addHttpClientSection($rootNode);
return $treeBuilder;
}
@ -1170,4 +1172,124 @@ class Configuration implements ConfigurationInterface
->end()
;
}
private function addHttpClientSection(ArrayNodeDefinition $rootNode)
{
$subNode = $rootNode
->children()
->arrayNode('http_client')
->info('HTTP Client configuration')
->canBeEnabled()
->fixXmlConfig('client')
->children();
$this->addHttpClientOptionsSection($subNode);
$subNode = $subNode
->arrayNode('clients')
->useAttributeAsKey('name')
->normalizeKeys(false)
->arrayPrototype()
->children();
$this->addHttpClientOptionsSection($subNode);
$subNode = $subNode
->end()
->end()
->end()
->end()
->end()
->end()
;
}
private function addHttpClientOptionsSection(NodeBuilder $rootNode)
{
$rootNode
->integerNode('max_host_connections')
->info('The maximum number of connections to a single host.')
->end()
->arrayNode('default_options')
->fixXmlConfig('header')
->children()
->scalarNode('auth')
->info('An HTTP Basic authentication "username:password".')
->end()
->arrayNode('query')
->info('Associative array of query string values merged with URL parameters.')
->useAttributeAsKey('key')
->normalizeKeys(false)
->scalarPrototype()->end()
->end()
->arrayNode('headers')
->info('Associative array: header => value(s).')
->useAttributeAsKey('name')
->normalizeKeys(false)
->variablePrototype()->end()
->end()
->integerNode('max_redirects')
->info('The maximum number of redirects to follow.')
->end()
->scalarNode('http_version')
->info('The default HTTP version, typically 1.1 or 2.0. Leave to null for the best version.')
->end()
->scalarNode('base_uri')
->info('The URI to resolve relative URLs, following rules in RFC 3986, section 2.')
->end()
->booleanNode('buffer')
->info('Indicates if the response should be buffered or not.')
->end()
->arrayNode('resolve')
->info('Associative array: domain => IP.')
->useAttributeAsKey('host')
->normalizeKeys(false)
->scalarPrototype()->end()
->end()
->scalarNode('proxy')
->info('The URL of the proxy to pass requests through or null for automatic detection.')
->end()
->scalarNode('no_proxy')
->info('A comma separated list of hosts that do not require a proxy to be reached.')
->end()
->floatNode('timeout')
->info('Defaults to "default_socket_timeout" ini parameter.')
->end()
->scalarNode('bindto')
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
->end()
->booleanNode('verify_peer')
->info('Indicates if the peer should be verified in a SSL/TLS context.')
->end()
->booleanNode('verify_host')
->info('Indicates if the host should exist as a certificate common name.')
->end()
->scalarNode('cafile')
->info('A certificate authority file.')
->end()
->scalarNode('capath')
->info('A directory that contains multiple certificate authority files.')
->end()
->scalarNode('local_cert')
->info('A PEM formatted certificate file.')
->end()
->scalarNode('local_pk')
->info('A private key file.')
->end()
->scalarNode('passphrase')
->info('The passphrase used to encrypt the "local_pk" file.')
->end()
->scalarNode('ciphers')
->info('A list of SSL/TLS ciphers separated by colons, commas or spaces (e.g. "RC4-SHA:TLS13-AES-128-GCM-SHA256"...)')
->end()
->arrayNode('peer_fingerprint')
->info('Associative array: hashing algorithm => hash(es).')
->useAttributeAsKey('algo')
->normalizeKeys(false)
->variablePrototype()->end()
->end()
->end()
->end()
;
}
}

View File

@ -15,6 +15,7 @@ use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\Reader;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerAwareInterface;
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
use Symfony\Bridge\Twig\Extension\CsrfExtension;
@ -57,6 +58,9 @@ use Symfony\Component\Finder\Finder;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\HttpClientTrait;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
@ -110,6 +114,8 @@ use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Service\ResetInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
@ -301,6 +307,10 @@ class FrameworkExtension extends Extension
$this->registerLockConfiguration($config['lock'], $container, $loader);
}
if ($this->isConfigEnabled($container, $config['http_client'])) {
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader);
}
if ($this->isConfigEnabled($container, $config['web_link'])) {
if (!class_exists(HttpHeaderSerializer::class)) {
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".');
@ -1747,6 +1757,48 @@ class FrameworkExtension extends Extension
}
}
private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
if (!class_exists(HttpClient::class)) {
throw new LogicException('HttpClient support cannot be enabled as the component is not installed. Try running "composer require symfony/http-client".');
}
$loader->load('http_client.xml');
$merger = new class() {
use HttpClientTrait;
public function merge(array $options, array $defaultOptions)
{
try {
[, $options] = $this->prepareRequest(null, null, $options, $defaultOptions);
return $options;
} catch (TransportExceptionInterface $e) {
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
}
};
$defaultOptions = $merger->merge($config['default_options'] ?? [], []);
$container->getDefinition('http_client')->setArguments([$defaultOptions, $config['max_host_connections'] ?? 6]);
foreach ($config['clients'] as $name => $clientConfig) {
$options = $merger->merge($clientConfig['default_options'] ?? [], $defaultOptions);
$container->register($name, HttpClientInterface::class)
->setFactory([HttpClient::class, 'create'])
->setArguments([$options, $clientConfig['max_host_connections'] ?? $config['max_host_connections'] ?? 6]);
$container->register('psr18.'.$name, Psr18Client::class)
->setAutowired(true)
->setArguments([new Reference($name)]);
$container->registerAliasForArgument($name, HttpClientInterface::class);
$container->registerAliasForArgument('psr18.'.$name, ClientInterface::class, $name);
}
}
/**
* Returns the base path for the XSD files.
*

View File

@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="http_client" class="Symfony\Contracts\HttpClient\HttpClientInterface">
<factory class="Symfony\Component\HttpClient\HttpClient" method="create" />
<argument type="collection" /> <!-- default options -->
<argument /> <!-- max host connections -->
</service>
<service id="Symfony\Contracts\HttpClient\HttpClientInterface" alias="http_client" />
<service id="psr18.http_client" class="Symfony\Component\HttpClient\Psr18Client" autowire="true">
<argument type="service" id="http_client" />
</service>
<service id="Psr\Http\Client\ClientInterface" alias="psr18.http_client" />
</services>
</container>

View File

@ -32,6 +32,7 @@
<xsd:element name="php-errors" type="php-errors" minOccurs="0" maxOccurs="1" />
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
<xsd:element name="messenger" type="messenger" minOccurs="0" maxOccurs="1" />
<xsd:element name="http_client" type="http_client" minOccurs="0" maxOccurs="1" />
</xsd:choice>
<xsd:attribute name="http-method-override" type="xsd:boolean" />
@ -444,4 +445,64 @@
</xsd:sequence>
<xsd:attribute name="id" type="xsd:string" use="required"/>
</xsd:complexType>
<xsd:complexType name="http_client">
<xsd:sequence>
<xsd:element name="max_host_connections" type="xsd:integer" minOccurs="0" />
<xsd:element name="default_options" type="http_client_options" minOccurs="0" />
<xsd:element name="client" type="http_client_client" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="enabled" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="http_client_options">
<xsd:sequence>
<xsd:element name="auth" type="xsd:string" minOccurs="0" />
<xsd:element name="query" type="http_query" minOccurs="0" />
<xsd:element name="headers" type="http_headers" minOccurs="0" />
<xsd:element name="max_redirects" type="xsd:integer" minOccurs="0" />
<xsd:element name="http_version" type="xsd:string" minOccurs="0" />
<xsd:element name="base_uri" type="xsd:string" minOccurs="0" />
<xsd:element name="buffer" type="xsd:boolean" minOccurs="0" />
<xsd:element name="resolve" type="metadata" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="proxy" type="xsd:string" minOccurs="0" />
<xsd:element name="no_proxy" type="xsd:string" minOccurs="0" />
<xsd:element name="timeout" type="xsd:float" minOccurs="0" />
<xsd:element name="bindto" type="xsd:string" minOccurs="0" />
<xsd:element name="verify_peer" type="xsd:boolean" minOccurs="0" />
<xsd:element name="verify_host" type="xsd:boolean" minOccurs="0" />
<xsd:element name="cafile" type="xsd:string" minOccurs="0" />
<xsd:element name="capath" type="xsd:string" minOccurs="0" />
<xsd:element name="local_cert" type="xsd:string" minOccurs="0" />
<xsd:element name="local_pk" type="xsd:string" minOccurs="0" />
<xsd:element name="passphrase" type="xsd:string" minOccurs="0" />
<xsd:element name="ciphers" type="xsd:string" minOccurs="0" />
<xsd:element name="peer_fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="http_client_client">
<xsd:sequence>
<xsd:element name="default_options" type="http_client_options" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="fingerprint">
<xsd:sequence>
<xsd:any minOccurs="0" processContents="lax" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="http_query">
<xsd:sequence>
<xsd:any minOccurs="0" processContents="lax" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="http_headers">
<xsd:sequence>
<xsd:any minOccurs="0" processContents="lax" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

View File

@ -331,6 +331,11 @@ class ConfigurationTest extends TestCase
'buses' => ['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]],
],
'disallow_search_engine_index' => true,
'http_client' => [
'enabled' => false,
'max_host_connections' => 6,
'clients' => [],
],
];
}
}

View File

@ -0,0 +1,13 @@
<?php
$container->loadFromExtension('framework', [
'http_client' => [
'max_host_connections' => 4,
'default_options' => null,
'clients' => [
'foo' => [
'default_options' => null,
],
],
],
]);

View File

@ -0,0 +1,31 @@
<?php
$container->loadFromExtension('framework', [
'http_client' => [
'default_options' => [
'auth' => 'foo:bar',
'query' => ['foo' => 'bar', 'bar' => 'baz'],
'headers' => ['X-powered' => 'PHP'],
'max_redirects' => 2,
'http_version' => '2.0',
'base_uri' => 'http://example.com',
'buffer' => true,
'resolve' => ['localhost' => '127.0.0.1'],
'proxy' => 'proxy.org',
'timeout' => 3.5,
'bindto' => '127.0.0.1',
'verify_peer' => true,
'verify_host' => true,
'cafile' => '/etc/ssl/cafile',
'capath' => '/etc/ssl',
'local_cert' => '/etc/ssl/cert.pem',
'local_pk' => '/etc/ssl/private_key.pem',
'passphrase' => 'password123456',
'ciphers' => 'RC4-SHA:TLS13-AES-128-GCM-SHA256',
'peer_fingerprint' => [
'pin-sha256' => ['14s5erg62v1v8471g2revg48r7==', 'jsda84hjtyd4821bgfesd215bsfg5412='],
'md5' => 'sdhtb481248721thbr=',
],
],
],
]);

View File

@ -0,0 +1,16 @@
<?php
$container->loadFromExtension('framework', [
'http_client' => [
'default_options' => [
'headers' => ['foo' => 'bar'],
],
'clients' => [
'foo' => [
'default_options' => [
'headers' => ['bar' => 'baz'],
],
],
],
],
]);

View File

@ -0,0 +1,17 @@
<?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:max_host_connections>4</framework:max_host_connections>
<framework:default_options />
<framework:client name="foo">
<framework:default_options />
</framework:client>
</framework:http_client>
</framework:config>
</container>

View File

@ -0,0 +1,45 @@
<?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:auth>foo:bar</framework:auth>
<framework:query>
<framework:foo>bar</framework:foo>
<framework:bar>baz</framework:bar>
</framework:query>
<framework:headers>
<framework:X-powered>PHP</framework:X-powered>
</framework:headers>
<framework:max_redirects>2</framework:max_redirects>
<framework:http_version>2.0</framework:http_version>
<framework:base_uri>http://example.com</framework:base_uri>
<framework:buffer>true</framework:buffer>
<framework:resolve>
<framework:localhost>127.0.0.1</framework:localhost>
</framework:resolve>
<framework:proxy>proxy.org</framework:proxy>
<framework:timeout>3.5</framework:timeout>
<framework:bindto>127.0.0.1</framework:bindto>
<framework:verify_peer>true</framework:verify_peer>
<framework:verify_host>true</framework:verify_host>
<framework:cafile>/etc/ssl/cafile</framework:cafile>
<framework:capath>/etc/ssl</framework:capath>
<framework:local_cert>/etc/ssl/cert.pem</framework:local_cert>
<framework:local_pk>/etc/ssl/private_key.pem</framework:local_pk>
<framework:passphrase>password123456</framework:passphrase>
<framework:ciphers>RC4-SHA:TLS13-AES-128-GCM-SHA256</framework:ciphers>
<framework:peer_fingerprint>
<framework:pin-sha256>14s5erg62v1v8471g2revg48r7==</framework:pin-sha256>
<framework:pin-sha256>jsda84hjtyd4821bgfesd215bsfg5412=</framework:pin-sha256>
<framework:md5>sdhtb481248721thbr=</framework:md5>
</framework:peer_fingerprint>
</framework:default_options>
</framework:http_client>
</framework:config>
</container>

View File

@ -0,0 +1,24 @@
<?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:headers>
<framework:foo>bar</framework:foo>
</framework:headers>
</framework:default_options>
<framework:client name="foo">
<framework:default_options>
<framework:headers>
<framework:bar>baz</framework:bar>
</framework:headers>
</framework:default_options>
</framework:client>
</framework:http_client>
</framework:config>
</container>

View File

@ -0,0 +1,7 @@
framework:
http_client:
max_host_connections: 4
default_options: ~
clients:
foo:
default_options: ~

View File

@ -0,0 +1,26 @@
framework:
http_client:
default_options:
auth: foo:bar
query: {'foo': 'bar', 'bar': 'baz'}
headers:
X-powered: PHP
max_redirects: 2
http_version: 2.0
base_uri: 'http://example.com'
buffer: true
resolve: {'localhost': '127.0.0.1'}
proxy: proxy.org
timeout: 3.5
bindto: 127.0.0.1
verify_peer: true
verify_host: true
cafile: /etc/ssl/cafile
capath: /etc/ssl
local_cert: /etc/ssl/cert.pem
local_pk: /etc/ssl/private_key.pem
passphrase: password123456
ciphers: 'RC4-SHA:TLS13-AES-128-GCM-SHA256'
peer_fingerprint:
pin-sha256: ['14s5erg62v1v8471g2revg48r7==', 'jsda84hjtyd4821bgfesd215bsfg5412=']
md5: 'sdhtb481248721thbr='

View File

@ -0,0 +1,8 @@
framework:
http_client:
default_options:
headers: {'foo': 'bar'}
clients:
foo:
default_options:
headers: {'bar': 'baz'}

View File

@ -51,6 +51,7 @@ use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
use Symfony\Component\Workflow;
use Symfony\Contracts\HttpClient\HttpClientInterface;
abstract class FrameworkExtensionTest extends TestCase
{
@ -1353,6 +1354,64 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertFalse($container->has('disallow_search_engine_index_response_listener'), 'DisallowRobotsIndexingListener should NOT be registered');
}
public function testHttpClientDefaultOptions()
{
$container = $this->createContainerFromFile('http_client_default_options');
$this->assertTrue($container->hasDefinition('http_client'), '->registerHttpClientConfiguration() loads http_client.xml');
$defaultOptions = [
'query' => [],
'headers' => [],
'resolve' => [],
'peer_fingerprint' => [],
'http_version' => '',
];
$this->assertSame([$defaultOptions, 4], $container->getDefinition('http_client')->getArguments());
$this->assertTrue($container->hasDefinition('foo'), 'should have the "foo" service.');
$this->assertSame(HttpClientInterface::class, $container->getDefinition('foo')->getClass());
$this->assertSame([$defaultOptions, 4], $container->getDefinition('foo')->getArguments());
}
public function testHttpClientOverrideDefaultOptions()
{
$container = $this->createContainerFromFile('http_client_override_default_options');
$this->assertSame(['foo' => ['bar']], $container->getDefinition('http_client')->getArguments()[0]['headers']);
$this->assertSame(['bar' => ['baz'], 'foo' => ['bar']], $container->getDefinition('foo')->getArguments()[0]['headers']);
}
public function testHttpClientFullDefaultOptions()
{
$container = $this->createContainerFromFile('http_client_full_default_options');
$defaultOptions = $container->getDefinition('http_client')->getArguments()[0];
$this->assertSame('foo:bar', $defaultOptions['auth']);
$this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $defaultOptions['query']);
$this->assertSame(['x-powered' => ['PHP']], $defaultOptions['headers']);
$this->assertSame(2, $defaultOptions['max_redirects']);
$this->assertSame(2.0, (float) $defaultOptions['http_version']);
$this->assertSame('http://example.com', $defaultOptions['base_uri']);
$this->assertTrue($defaultOptions['buffer']);
$this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']);
$this->assertSame('proxy.org', $defaultOptions['proxy']);
$this->assertSame(3.5, $defaultOptions['timeout']);
$this->assertSame('127.0.0.1', $defaultOptions['bindto']);
$this->assertTrue($defaultOptions['verify_peer']);
$this->assertTrue($defaultOptions['verify_host']);
$this->assertSame('/etc/ssl/cafile', $defaultOptions['cafile']);
$this->assertSame('/etc/ssl', $defaultOptions['capath']);
$this->assertSame('/etc/ssl/cert.pem', $defaultOptions['local_cert']);
$this->assertSame('/etc/ssl/private_key.pem', $defaultOptions['local_pk']);
$this->assertSame('password123456', $defaultOptions['passphrase']);
$this->assertSame('RC4-SHA:TLS13-AES-128-GCM-SHA256', $defaultOptions['ciphers']);
$this->assertSame([
'pin-sha256' => ['14s5erg62v1v8471g2revg48r7==', 'jsda84hjtyd4821bgfesd215bsfg5412='],
'md5' => 'sdhtb481248721thbr=',
], $defaultOptions['peer_fingerprint']);
}
protected function createContainer(array $data = [])
{
return new ContainerBuilder(new ParameterBag(array_merge([

View File

@ -42,6 +42,7 @@
"symfony/security": "~3.4|~4.0",
"symfony/form": "^4.3",
"symfony/expression-language": "~3.4|~4.0",
"symfony/http-client": "^4.3",
"symfony/messenger": "^4.2",
"symfony/mime": "^4.3",
"symfony/process": "~3.4|~4.0",

View File

@ -53,7 +53,7 @@ final class CurlHttpClient implements HttpClientInterface
if (\defined('CURLPIPE_MULTIPLEX')) {
curl_multi_setopt($mh, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
}
curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections);
curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : PHP_INT_MAX);
// Use an internal stdClass object to share state between the client and its responses
$this->multi = $multi = (object) [

View File

@ -141,7 +141,9 @@ trait HttpClientTrait
// Option "query" is never inherited from defaults
$options['query'] = $options['query'] ?? [];
$options += $defaultOptions;
foreach ($defaultOptions as $k => $v) {
$options[$k] = $options[$k] ?? $v;
}
if ($defaultOptions['resolve'] ?? false) {
$options['resolve'] += array_change_key_case($defaultOptions['resolve']);

View File

@ -52,7 +52,7 @@ final class NativeHttpClient implements HttpClientInterface
'openHandles' => [],
'handlesActivity' => [],
'pendingResponses' => [],
'maxHostConnections' => $maxHostConnections,
'maxHostConnections' => 0 < $maxHostConnections ? $maxHostConnections : PHP_INT_MAX,
'responseCount' => 0,
'dnsCache' => [],
'handles' => [],