diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 1b23702913404..b30d86169b43b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -17,12 +17,12 @@
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 @@
*/
class Configuration implements ConfigurationInterface
{
+ use HttpClientTrait;
+
private $debug;
/**
@@ -1232,144 +1234,231 @@ private function addRobotsIndexSection(ArrayNodeDefinition $rootNode)
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();
-
- $this->addHttpClientOptionsSection($subNode);
-
- $subNode = $subNode
- ->arrayNode('clients')
+ ->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;
+ }
+
+ 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()
+ ->arrayNode('scoped_clients')
->useAttributeAsKey('name')
->normalizeKeys(false)
->arrayPrototype()
- ->children();
+ ->fixXmlConfig('header')
+ ->beforeNormalization()
+ ->always()
+ ->then(function ($config) {
+ $config = \is_array($config) ? $config : ['base_uri' => $config];
- $this->addHttpClientOptionsSection($subNode);
+ if (!isset($config['scope']) && isset($config['base_uri'])) {
+ $config['scope'] = preg_quote(implode('', self::resolveUrl(self::parseUrl('.'), self::parseUrl($config['base_uri']))));
+ }
- $subNode = $subNode
+ return $config;
+ })
->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_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;
- }
+ ->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['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['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. "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['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 71c02ad80e3ad..3b59605d16609 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\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\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 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
$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));
+ }
+
+ $scope = $scopeConfig['scope'];
+ unset($scopeConfig['scope']);
- $container->register($name, HttpClientInterface::class)
- ->setFactory([HttpClient::class, 'create'])
- ->setArguments([$options, $clientConfig['max_host_connections'] ?? $config['max_host_connections'] ?? 6]);
+ $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 0415fa9559c54..38e60f6516846 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,55 @@
-
-
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
@@ -491,14 +516,6 @@
-
-
-
-
-
-
-
-
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index bc1ee582fc081..aa0a2fc921853 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -336,7 +336,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'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 bd36ab1f03d15..5f71a92847f34 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 @@
'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 59e7f85d03c23..04a227c24cb14 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 26b76359da3fb..8ba8dd7b92ec8 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 @@
'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 5a16c54914c3a..c00eb314415b9 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 6f889ba6e8715..2ea78874d2176 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 085b4721cc7d8..8dd84123ca4b5 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 4abf1b897380d..6828f8ec231fb 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 3d18286820e05..5993be1778fe6 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 9a3d69e3585b4..1528a313d64e3 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 e62651a40fced..acc7fbad156e7 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\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\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 @@ public function testHttpClientDefaultOptions()
$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 @@ public function testHttpClientFullDefaultOptions()
$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 1e7581a5c1f53..9ca47e6624290 100644
--- a/src/Symfony/Component/HttpClient/Response/MockResponse.php
+++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php
@@ -240,7 +240,7 @@ private static function readResponse(self $response, array $options, ResponseInt
$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 @@ private static function readResponse(self $response, array $options, ResponseInt
// "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 9e07221933096..cf44d0eceba8e 100644
--- a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php
+++ b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php
@@ -122,8 +122,7 @@ public function set($key, $values, $replace = true)
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 29a6a97cefe22..2c04e670cc05f 100644
--- a/src/Symfony/Component/HttpKernel/HttpClientKernel.php
+++ b/src/Symfony/Component/HttpKernel/HttpClientKernel.php
@@ -16,6 +16,7 @@
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 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
$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