diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 1b23702913..b30d86169b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -17,12 +17,12 @@ 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; use Symfony\Component\Form\Form; use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpClient\HttpClientTrait; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\Store\SemaphoreStore; @@ -43,6 +43,8 @@ use Symfony\Component\WebLink\HttpHeaderSerializer; */ class Configuration implements ConfigurationInterface { + use HttpClientTrait; + private $debug; /** @@ -1232,144 +1234,231 @@ class Configuration implements ConfigurationInterface private function addHttpClientSection(ArrayNodeDefinition $rootNode) { - $subNode = $rootNode + $rootNode ->children() ->arrayNode('http_client') ->info('HTTP Client configuration') ->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}() - ->fixXmlConfig('client') - ->children(); + ->fixXmlConfig('scoped_client') + ->children() + ->integerNode('max_host_connections') + ->info('The maximum number of connections to a single host.') + ->end() + ->arrayNode('default_options') + ->fixXmlConfig('header') + ->children() + ->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() + ->arrayNode('resolve') + ->info('Associative array: domain => IP.') + ->useAttributeAsKey('host') + ->beforeNormalization() + ->always(function ($config) { + if (!\is_array($config)) { + return []; + } + if (!isset($config['host'])) { + return $config; + } - $this->addHttpClientOptionsSection($subNode); - - $subNode = $subNode - ->arrayNode('clients') - ->useAttributeAsKey('name') - ->normalizeKeys(false) - ->arrayPrototype() - ->children(); - - $this->addHttpClientOptionsSection($subNode); - - $subNode = $subNode + return [$config['host'] => $config['value']]; + }) + ->end() + ->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. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)') + ->end() + ->arrayNode('peer_fingerprint') + ->info('Associative array: hashing algorithm => hash(es).') + ->normalizeKeys(false) + ->children() + ->variableNode('sha1')->end() + ->variableNode('pin-sha256')->end() + ->variableNode('md5')->end() + ->end() ->end() ->end() ->end() - ->end() - ->end() - ->end() - ; - } + ->arrayNode('scoped_clients') + ->useAttributeAsKey('name') + ->normalizeKeys(false) + ->arrayPrototype() + ->fixXmlConfig('header') + ->beforeNormalization() + ->always() + ->then(function ($config) { + $config = \is_array($config) ? $config : ['base_uri' => $config]; - 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_basic') - ->info('An HTTP Basic authentication "username:password".') - ->end() - ->scalarNode('auth_bearer') - ->info('A token enabling HTTP Bearer authorization.') - ->end() - ->arrayNode('query') - ->info('Associative array of query string values merged with URL parameters.') - ->useAttributeAsKey('key') - ->beforeNormalization() - ->always(function ($config) { - if (!\is_array($config)) { - return []; - } - if (!isset($config['key'])) { - return $config; - } + if (!isset($config['scope']) && isset($config['base_uri'])) { + $config['scope'] = preg_quote(implode('', self::resolveUrl(self::parseUrl('.'), self::parseUrl($config['base_uri'])))); + } - return [$config['key'] => $config['value']]; - }) - ->end() - ->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() - ->arrayNode('resolve') - ->info('Associative array: domain => IP.') - ->useAttributeAsKey('host') - ->beforeNormalization() - ->always(function ($config) { - if (!\is_array($config)) { - return []; - } - if (!isset($config['host'])) { - return $config; - } + return $config; + }) + ->end() + ->validate() + ->ifTrue(function ($v) { return !isset($v['scope']); }) + ->thenInvalid('either "scope" or "base_uri" should be defined.') + ->end() + ->validate() + ->ifTrue(function ($v) { return isset($v['query']) && !isset($v['base_uri']); }) + ->thenInvalid('"query" applies to "base_uri" but no base URI is defined.') + ->end() + ->children() + ->scalarNode('scope') + ->info('The regular expression that the request URL must match before adding the other options. When none is provided, the base URI is used instead.') + ->cannotBeEmpty() + ->end() + ->scalarNode('base_uri') + ->info('The URI to resolve relative URLs, following rules in RFC 3985, section 2.') + ->cannotBeEmpty() + ->end() + ->scalarNode('auth_basic') + ->info('An HTTP Basic authentication "username:password".') + ->end() + ->scalarNode('auth_bearer') + ->info('A token enabling HTTP Bearer authorization.') + ->end() + ->arrayNode('query') + ->info('Associative array of query string values merged with the base URI.') + ->useAttributeAsKey('key') + ->beforeNormalization() + ->always(function ($config) { + if (!\is_array($config)) { + return []; + } + if (!isset($config['key'])) { + return $config; + } - return [$config['host'] => $config['value']]; - }) - ->end() - ->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).') - ->normalizeKeys(false) - ->children() - ->variableNode('sha1')->end() - ->variableNode('pin-sha256')->end() - ->variableNode('md5')->end() + return [$config['key'] => $config['value']]; + }) + ->end() + ->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() + ->arrayNode('resolve') + ->info('Associative array: domain => IP.') + ->useAttributeAsKey('host') + ->beforeNormalization() + ->always(function ($config) { + if (!\is_array($config)) { + return []; + } + if (!isset($config['host'])) { + return $config; + } + + return [$config['host'] => $config['value']]; + }) + ->end() + ->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. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)') + ->end() + ->arrayNode('peer_fingerprint') + ->info('Associative array: hashing algorithm => hash(es).') + ->normalizeKeys(false) + ->children() + ->variableNode('sha1')->end() + ->variableNode('pin-sha256')->end() + ->variableNode('md5')->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 71c02ad80e..3b59605d16 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -61,8 +61,8 @@ 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\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -117,7 +117,6 @@ 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; @@ -1803,42 +1802,23 @@ class FrameworkExtension extends Extension $loader->load('http_client.xml'); - $merger = new class() { - use HttpClientTrait; - - public function merge(array $options, array $defaultOptions) - { - try { - [, $mergedOptions] = $this->prepareRequest(null, null, $options, $defaultOptions); - - foreach ($mergedOptions as $k => $v) { - if (!isset($options[$k]) && !isset($defaultOptions[$k])) { - // Remove options added by prepareRequest() - unset($mergedOptions[$k]); - } - } - - return $mergedOptions; - } 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]); + $container->getDefinition('http_client')->setArguments([$config['default_options'] ?? [], $config['max_host_connections'] ?? 6]); if (!$hasPsr18 = interface_exists(ClientInterface::class)) { $container->removeDefinition('psr18.http_client'); $container->removeAlias(ClientInterface::class); } - foreach ($config['clients'] as $name => $clientConfig) { - $options = $merger->merge($clientConfig['default_options'] ?? [], $defaultOptions); + foreach ($config['scoped_clients'] as $name => $scopeConfig) { + if ('http_client' === $name) { + throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name)); + } - $container->register($name, HttpClientInterface::class) - ->setFactory([HttpClient::class, 'create']) - ->setArguments([$options, $clientConfig['max_host_connections'] ?? $config['max_host_connections'] ?? 6]); + $scope = $scopeConfig['scope']; + unset($scopeConfig['scope']); + + $container->register($name, ScopingHttpClient::class) + ->setArguments([new Reference('http_client'), [$scope => $scopeConfig], $scope]); $container->registerAliasForArgument($name, HttpClientInterface::class); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 0415fa9559..38e60f6516 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -458,30 +458,26 @@ - - + + - + - + + + - - - - - - @@ -489,14 +485,35 @@ + - - - - + + + + + + + - + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index bc1ee582fc..aa0a2fc921 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -336,7 +336,7 @@ class ConfigurationTest extends TestCase 'disallow_search_engine_index' => true, 'http_client' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class), - 'clients' => [], + 'scoped_clients' => [], ], 'mailer' => [ 'dsn' => 'smtp://null', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php index bd36ab1f03..5f71a92847 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_default_options.php @@ -4,9 +4,9 @@ $container->loadFromExtension('framework', [ 'http_client' => [ 'max_host_connections' => 4, 'default_options' => null, - 'clients' => [ + 'scoped_clients' => [ 'foo' => [ - 'default_options' => null, + 'base_uri' => 'http://example.com', ], ], ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php index 59e7f85d03..04a227c24c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php @@ -3,12 +3,9 @@ $container->loadFromExtension('framework', [ 'http_client' => [ 'default_options' => [ - 'auth_basic' => 'foo:bar', - 'query' => ['foo' => 'bar', 'bar' => 'baz'], 'headers' => ['X-powered' => 'PHP'], 'max_redirects' => 2, 'http_version' => '2.0', - 'base_uri' => 'http://example.com', 'resolve' => ['localhost' => '127.0.0.1'], 'proxy' => 'proxy.org', 'timeout' => 3.5, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php index 26b76359da..8ba8dd7b92 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_override_default_options.php @@ -6,12 +6,10 @@ $container->loadFromExtension('framework', [ 'default_options' => [ 'headers' => ['foo' => 'bar'], ], - 'clients' => [ + 'scoped_clients' => [ 'foo' => [ - 'max_host_connections' => 5, - 'default_options' => [ - 'headers' => ['bar' => 'baz'], - ], + 'base_uri' => 'http://example.com', + 'headers' => ['bar' => 'baz'], ], ], ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml index 5a16c54914..c00eb31441 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_default_options.xml @@ -8,9 +8,10 @@ - - - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml index 6f889ba6e8..2ea78874d2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml @@ -12,10 +12,8 @@ bindto="127.0.0.1" timeout="3.5" verify-peer="true" - auth-basic="foo:bar" max-redirects="2" http-version="2.0" - base-uri="http://example.com" verify-host="true" cafile="/etc/ssl/cafile" capath="/etc/ssl" @@ -24,8 +22,6 @@ passphrase="password123456" ciphers="RC4-SHA:TLS13-AES-128-GCM-SHA256" > - bar - baz PHP 127.0.0.1 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml index 085b4721cc..8dd84123ca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_override_default_options.xml @@ -10,11 +10,9 @@ bar - - - baz - - + + baz + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml index 4abf1b8973..6828f8ec23 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_default_options.yml @@ -2,6 +2,6 @@ framework: http_client: max_host_connections: 4 default_options: ~ - clients: + scoped_clients: foo: - default_options: ~ + base_uri: http://example.com diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml index 3d18286820..5993be1778 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml @@ -1,13 +1,10 @@ framework: http_client: default_options: - auth_basic: foo:bar - query: {'foo': 'bar', 'bar': 'baz'} headers: X-powered: PHP max_redirects: 2 http_version: 2.0 - base_uri: 'http://example.com' resolve: {'localhost': '127.0.0.1'} proxy: proxy.org timeout: 3.5 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml index 9a3d69e358..1528a313d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_override_default_options.yml @@ -3,8 +3,7 @@ framework: max_host_connections: 4 default_options: headers: {'foo': 'bar'} - clients: + scoped_clients: foo: - max_host_connections: 5 - default_options: - headers: {'bar': 'baz'} + base_uri: http://example.com + headers: {'bar': 'baz'} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index e62651a40f..acc7fbad15 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -35,6 +35,7 @@ use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage; @@ -53,7 +54,6 @@ use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\Validation; use Symfony\Component\Workflow; -use Symfony\Contracts\HttpClient\HttpClientInterface; abstract class FrameworkExtensionTest extends TestCase { @@ -1406,25 +1406,34 @@ abstract class FrameworkExtensionTest extends TestCase $this->assertTrue($container->hasDefinition('http_client'), '->registerHttpClientConfiguration() loads http_client.xml'); $defaultOptions = [ - 'query' => [], 'headers' => [], 'resolve' => [], ]; $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()); + $this->assertSame(ScopingHttpClient::class, $container->getDefinition('foo')->getClass()); } public function testHttpClientOverrideDefaultOptions() { $container = $this->createContainerFromFile('http_client_override_default_options'); - $this->assertSame(['foo' => ['bar']], $container->getDefinition('http_client')->getArgument(0)['headers']); + $this->assertSame(['foo' => 'bar'], $container->getDefinition('http_client')->getArgument(0)['headers']); $this->assertSame(4, $container->getDefinition('http_client')->getArgument(1)); - $this->assertSame(['bar' => ['baz'], 'foo' => ['bar']], $container->getDefinition('foo')->getArgument(0)['headers']); - $this->assertSame(5, $container->getDefinition('foo')->getArgument(1)); + + $expected = [ + 'http\://example\.com/' => [ + 'base_uri' => 'http://example.com', + 'headers' => [ + 'bar' => 'baz', + ], + 'query' => [], + 'resolve' => [], + ], + ]; + + $this->assertSame($expected, $container->getDefinition('foo')->getArgument(1)); } public function testHttpClientFullDefaultOptions() @@ -1433,12 +1442,9 @@ abstract class FrameworkExtensionTest extends TestCase $defaultOptions = $container->getDefinition('http_client')->getArgument(0); - $this->assertSame('foo:bar', $defaultOptions['auth_basic']); - $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $defaultOptions['query']); - $this->assertSame(['x-powered' => ['PHP']], $defaultOptions['headers']); + $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->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']); $this->assertSame('proxy.org', $defaultOptions['proxy']); $this->assertSame(3.5, $defaultOptions['timeout']); diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index 1e7581a5c1..9ca47e6624 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -240,7 +240,7 @@ class MockResponse implements ResponseInterface $info = $mock->getInfo() ?: []; $response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode(false) ?: 200; $response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers); - $dlSize = (int) ($response->headers['content-length'][0] ?? 0); + $dlSize = isset($response->headers['content-encoding']) ? 0 : (int) ($response->headers['content-length'][0] ?? 0); $response->info = [ 'start_time' => $response->info['start_time'], @@ -282,7 +282,7 @@ class MockResponse implements ResponseInterface // "notify" completion $onProgress($offset, $dlSize, $response->info); - if (isset($response->headers['content-length']) && $offset !== $dlSize) { + if ($dlSize && $offset !== $dlSize) { throw new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $dlSize - $offset)); } } diff --git a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php index 9e07221933..cf44d0eceb 100644 --- a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php @@ -122,8 +122,7 @@ class ResponseHeaderBag extends HeaderBag parent::set($key, $values, $replace); // ensure the cache-control header has sensible defaults - if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true)) { - $computed = $this->computeCacheControlValue(); + if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) { $this->headers['cache-control'] = [$computed]; $this->headerNames['cache-control'] = 'Cache-Control'; $this->computedCacheControl = $this->parseCacheControl($computed); diff --git a/src/Symfony/Component/HttpKernel/HttpClientKernel.php b/src/Symfony/Component/HttpKernel/HttpClientKernel.php index 29a6a97cef..2c04e670cc 100644 --- a/src/Symfony/Component/HttpKernel/HttpClientKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpClientKernel.php @@ -16,6 +16,7 @@ use Psr\Log\NullLogger; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Mime\Part\AbstractPart; use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\Multipart\FormDataPart; @@ -60,7 +61,16 @@ final class HttpClientKernel implements HttpKernelInterface $this->logger->debug(sprintf('Response: %s %s', $response->getStatusCode(), $request->getUri())); - return new Response($response->getContent(!$catch), $response->getStatusCode(), $response->getHeaders(!$catch)); + $response = new Response($response->getContent(!$catch), $response->getStatusCode(), $response->getHeaders(!$catch)); + + $response->headers = new class($response->headers->all()) extends ResponseHeaderBag { + protected function computeCacheControlValue() + { + return $this->getCacheControlHeader(); // preserve the original value + } + }; + + return $response; } private function getBody(Request $request): ?AbstractPart