feature #30674 [FrameworkBundle] change the way http clients are configured by leveraging ScopingHttpClient (nicolas-grekas)
This PR was merged into the 4.3-dev branch.
Discussion
----------
[FrameworkBundle] change the way http clients are configured by leveraging ScopingHttpClient
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
This PR allows configuring scoped HTTP clients ("scoped_clients" replaces the previous "clients" options):
```yaml
framework:
http_client:
max_host_connections: 4
default_options:
# ...
scoped_clients:
github_client:
base_uri: https://api.github.com
headers:
Authorization: token abc123
# ...
```
The base URI is turned into a scoping regular expression so that the token will be sent only when the `github_client` service is requesting the corresponding URLs.
When the base URI is too restrictive, the `scope` option can be used explicitly to define the regexp that URLs must match before any other options are applied.
~All defined scopes are passed to a new `scoping_http_client` service, that can be used to hit endpoints with authentication pre-configured for several hosts. Its named autowiring alias is `HttpClientInterface $scopingClient` (this cannot be done with `http_client` as we want safe defaults, e.g. credentials should not be used implicitly when writing webhooks/crawlers.)~
Commits
-------
f1a26b9aea
[FrameworkBundle] change the way http clients are configured by leveraging ScopingHttpClient
This commit is contained in:
commit
bce6124f8f
@ -17,12 +17,12 @@ 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\DependencyInjection\Exception\LogicException;
|
use Symfony\Component\DependencyInjection\Exception\LogicException;
|
||||||
use Symfony\Component\Form\Form;
|
use Symfony\Component\Form\Form;
|
||||||
use Symfony\Component\HttpClient\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Component\HttpClient\HttpClientTrait;
|
||||||
use Symfony\Component\HttpFoundation\Cookie;
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
use Symfony\Component\Lock\Lock;
|
use Symfony\Component\Lock\Lock;
|
||||||
use Symfony\Component\Lock\Store\SemaphoreStore;
|
use Symfony\Component\Lock\Store\SemaphoreStore;
|
||||||
@ -43,6 +43,8 @@ use Symfony\Component\WebLink\HttpHeaderSerializer;
|
|||||||
*/
|
*/
|
||||||
class Configuration implements ConfigurationInterface
|
class Configuration implements ConfigurationInterface
|
||||||
{
|
{
|
||||||
|
use HttpClientTrait;
|
||||||
|
|
||||||
private $debug;
|
private $debug;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1232,68 +1234,19 @@ class Configuration implements ConfigurationInterface
|
|||||||
|
|
||||||
private function addHttpClientSection(ArrayNodeDefinition $rootNode)
|
private function addHttpClientSection(ArrayNodeDefinition $rootNode)
|
||||||
{
|
{
|
||||||
$subNode = $rootNode
|
$rootNode
|
||||||
->children()
|
->children()
|
||||||
->arrayNode('http_client')
|
->arrayNode('http_client')
|
||||||
->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('client')
|
->fixXmlConfig('scoped_client')
|
||||||
->children();
|
->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')
|
->integerNode('max_host_connections')
|
||||||
->info('The maximum number of connections to a single host.')
|
->info('The maximum number of connections to a single host.')
|
||||||
->end()
|
->end()
|
||||||
->arrayNode('default_options')
|
->arrayNode('default_options')
|
||||||
->fixXmlConfig('header')
|
->fixXmlConfig('header')
|
||||||
->children()
|
->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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$config['key'] => $config['value']];
|
|
||||||
})
|
|
||||||
->end()
|
|
||||||
->normalizeKeys(false)
|
|
||||||
->scalarPrototype()->end()
|
|
||||||
->end()
|
|
||||||
->arrayNode('headers')
|
->arrayNode('headers')
|
||||||
->info('Associative array: header => value(s).')
|
->info('Associative array: header => value(s).')
|
||||||
->useAttributeAsKey('name')
|
->useAttributeAsKey('name')
|
||||||
@ -1304,10 +1257,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->info('The maximum number of redirects to follow.')
|
->info('The maximum number of redirects to follow.')
|
||||||
->end()
|
->end()
|
||||||
->scalarNode('http_version')
|
->scalarNode('http_version')
|
||||||
->info('The default HTTP version, typically 1.1 or 2.0. Leave to null for the best 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()
|
->end()
|
||||||
->arrayNode('resolve')
|
->arrayNode('resolve')
|
||||||
->info('Associative array: domain => IP.')
|
->info('Associative array: domain => IP.')
|
||||||
@ -1361,7 +1311,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->info('The passphrase used to encrypt the "local_pk" file.')
|
->info('The passphrase used to encrypt the "local_pk" file.')
|
||||||
->end()
|
->end()
|
||||||
->scalarNode('ciphers')
|
->scalarNode('ciphers')
|
||||||
->info('A list of SSL/TLS ciphers separated by colons, commas or spaces (e.g. "RC4-SHA:TLS13-AES-128-GCM-SHA256"...)')
|
->info('A list of SSL/TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)')
|
||||||
->end()
|
->end()
|
||||||
->arrayNode('peer_fingerprint')
|
->arrayNode('peer_fingerprint')
|
||||||
->info('Associative array: hashing algorithm => hash(es).')
|
->info('Associative array: hashing algorithm => hash(es).')
|
||||||
@ -1374,6 +1324,145 @@ class Configuration implements ConfigurationInterface
|
|||||||
->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];
|
||||||
|
|
||||||
|
if (!isset($config['scope']) && isset($config['base_uri'])) {
|
||||||
|
$config['scope'] = preg_quote(implode('', self::resolveUrl(self::parseUrl('.'), self::parseUrl($config['base_uri']))));
|
||||||
|
}
|
||||||
|
|
||||||
|
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['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()
|
||||||
|
->end()
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ 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\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\HttpClient\HttpClientTrait;
|
|
||||||
use Symfony\Component\HttpClient\Psr18Client;
|
use Symfony\Component\HttpClient\Psr18Client;
|
||||||
|
use Symfony\Component\HttpClient\ScopingHttpClient;
|
||||||
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
|
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
|
||||||
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
|
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
|
||||||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
|
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\Command\LintCommand as BaseYamlLintCommand;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
use Symfony\Contracts\Cache\CacheInterface;
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Symfony\Contracts\Service\ResetInterface;
|
use Symfony\Contracts\Service\ResetInterface;
|
||||||
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
||||||
@ -1803,42 +1802,23 @@ class FrameworkExtension extends Extension
|
|||||||
|
|
||||||
$loader->load('http_client.xml');
|
$loader->load('http_client.xml');
|
||||||
|
|
||||||
$merger = new class() {
|
$container->getDefinition('http_client')->setArguments([$config['default_options'] ?? [], $config['max_host_connections'] ?? 6]);
|
||||||
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]);
|
|
||||||
|
|
||||||
if (!$hasPsr18 = interface_exists(ClientInterface::class)) {
|
if (!$hasPsr18 = interface_exists(ClientInterface::class)) {
|
||||||
$container->removeDefinition('psr18.http_client');
|
$container->removeDefinition('psr18.http_client');
|
||||||
$container->removeAlias(ClientInterface::class);
|
$container->removeAlias(ClientInterface::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($config['clients'] as $name => $clientConfig) {
|
foreach ($config['scoped_clients'] as $name => $scopeConfig) {
|
||||||
$options = $merger->merge($clientConfig['default_options'] ?? [], $defaultOptions);
|
if ('http_client' === $name) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name));
|
||||||
|
}
|
||||||
|
|
||||||
$container->register($name, HttpClientInterface::class)
|
$scope = $scopeConfig['scope'];
|
||||||
->setFactory([HttpClient::class, 'create'])
|
unset($scopeConfig['scope']);
|
||||||
->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);
|
$container->registerAliasForArgument($name, HttpClientInterface::class);
|
||||||
|
|
||||||
|
@ -458,30 +458,26 @@
|
|||||||
|
|
||||||
<xsd:complexType name="http_client">
|
<xsd:complexType name="http_client">
|
||||||
<xsd:sequence>
|
<xsd:sequence>
|
||||||
<xsd:element name="default-options" type="http_client_options" minOccurs="0" />
|
<xsd:element name="default-options" type="http_client_default_options" minOccurs="0" />
|
||||||
<xsd:element name="client" type="http_client_client" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="scoped-client" type="http_client_scope_options" minOccurs="0" maxOccurs="unbounded" />
|
||||||
</xsd:sequence>
|
</xsd:sequence>
|
||||||
<xsd:attribute name="enabled" type="xsd:boolean" />
|
<xsd:attribute name="enabled" type="xsd:boolean" />
|
||||||
<xsd:attribute name="max-host-connections" type="xsd:integer" />
|
<xsd:attribute name="max-host-connections" type="xsd:integer" />
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
|
||||||
<xsd:complexType name="http_client_options" mixed="true">
|
<xsd:complexType name="http_client_default_options" mixed="true">
|
||||||
<xsd:choice maxOccurs="unbounded">
|
<xsd:choice maxOccurs="unbounded">
|
||||||
<xsd:element name="query" type="http_query" minOccurs="0" maxOccurs="unbounded" />
|
|
||||||
<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:choice>
|
</xsd:choice>
|
||||||
|
<xsd:attribute name="max-redirects" type="xsd:integer" />
|
||||||
|
<xsd:attribute name="http-version" type="xsd:string" />
|
||||||
<xsd:attribute name="proxy" type="xsd:string" />
|
<xsd:attribute name="proxy" type="xsd:string" />
|
||||||
|
<xsd:attribute name="no-proxy" type="xsd:string" />
|
||||||
<xsd:attribute name="timeout" type="xsd:float" />
|
<xsd:attribute name="timeout" type="xsd:float" />
|
||||||
<xsd:attribute name="bindto" type="xsd:string" />
|
<xsd:attribute name="bindto" type="xsd:string" />
|
||||||
<xsd:attribute name="verify-peer" type="xsd:boolean" />
|
<xsd:attribute name="verify-peer" type="xsd:boolean" />
|
||||||
<xsd:attribute name="auth-basic" type="xsd:string" />
|
|
||||||
<xsd:attribute name="auth-bearer" type="xsd:string" />
|
|
||||||
<xsd:attribute name="max-redirects" type="xsd:integer" />
|
|
||||||
<xsd:attribute name="http-version" type="xsd:string" />
|
|
||||||
<xsd:attribute name="base-uri" type="xsd:string" />
|
|
||||||
<xsd:attribute name="no-proxy" type="xsd:string" />
|
|
||||||
<xsd:attribute name="verify-host" type="xsd:boolean" />
|
<xsd:attribute name="verify-host" type="xsd:boolean" />
|
||||||
<xsd:attribute name="cafile" type="xsd:string" />
|
<xsd:attribute name="cafile" type="xsd:string" />
|
||||||
<xsd:attribute name="capath" type="xsd:string" />
|
<xsd:attribute name="capath" type="xsd:string" />
|
||||||
@ -489,14 +485,35 @@
|
|||||||
<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_client">
|
<xsd:complexType name="http_client_scope_options" mixed="true">
|
||||||
<xsd:sequence>
|
<xsd:choice maxOccurs="unbounded">
|
||||||
<xsd:element name="default-options" type="http_client_options" minOccurs="0" />
|
<xsd:element name="query" type="http_query" minOccurs="0" maxOccurs="unbounded" />
|
||||||
</xsd:sequence>
|
<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="peer-fingerprint" type="fingerprint" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
</xsd:choice>
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
<xsd:attribute name="max-host-connections" type="xsd:integer" />
|
<xsd:attribute name="scope" type="xsd:string" />
|
||||||
|
<xsd:attribute name="base-uri" type="xsd:string" />
|
||||||
|
<xsd:attribute name="auth-basic" type="xsd:string" />
|
||||||
|
<xsd:attribute name="auth-bearer" type="xsd:string" />
|
||||||
|
<xsd:attribute name="max-redirects" type="xsd:integer" />
|
||||||
|
<xsd:attribute name="http-version" type="xsd:string" />
|
||||||
|
<xsd:attribute name="proxy" type="xsd:string" />
|
||||||
|
<xsd:attribute name="no-proxy" type="xsd:string" />
|
||||||
|
<xsd:attribute name="timeout" type="xsd:float" />
|
||||||
|
<xsd:attribute name="bindto" type="xsd:string" />
|
||||||
|
<xsd:attribute name="verify-peer" type="xsd:boolean" />
|
||||||
|
<xsd:attribute name="verify-host" type="xsd:boolean" />
|
||||||
|
<xsd:attribute name="cafile" type="xsd:string" />
|
||||||
|
<xsd:attribute name="capath" type="xsd:string" />
|
||||||
|
<xsd:attribute name="local-cert" type="xsd:string" />
|
||||||
|
<xsd:attribute name="local-pk" type="xsd:string" />
|
||||||
|
<xsd:attribute name="passphrase" type="xsd:string" />
|
||||||
|
<xsd:attribute name="ciphers" type="xsd:string" />
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
|
||||||
<xsd:complexType name="fingerprint">
|
<xsd:complexType name="fingerprint">
|
||||||
|
@ -336,7 +336,7 @@ class ConfigurationTest extends TestCase
|
|||||||
'disallow_search_engine_index' => true,
|
'disallow_search_engine_index' => true,
|
||||||
'http_client' => [
|
'http_client' => [
|
||||||
'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class),
|
'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class),
|
||||||
'clients' => [],
|
'scoped_clients' => [],
|
||||||
],
|
],
|
||||||
'mailer' => [
|
'mailer' => [
|
||||||
'dsn' => 'smtp://null',
|
'dsn' => 'smtp://null',
|
||||||
|
@ -4,9 +4,9 @@ $container->loadFromExtension('framework', [
|
|||||||
'http_client' => [
|
'http_client' => [
|
||||||
'max_host_connections' => 4,
|
'max_host_connections' => 4,
|
||||||
'default_options' => null,
|
'default_options' => null,
|
||||||
'clients' => [
|
'scoped_clients' => [
|
||||||
'foo' => [
|
'foo' => [
|
||||||
'default_options' => null,
|
'base_uri' => 'http://example.com',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -3,12 +3,9 @@
|
|||||||
$container->loadFromExtension('framework', [
|
$container->loadFromExtension('framework', [
|
||||||
'http_client' => [
|
'http_client' => [
|
||||||
'default_options' => [
|
'default_options' => [
|
||||||
'auth_basic' => 'foo:bar',
|
|
||||||
'query' => ['foo' => 'bar', 'bar' => 'baz'],
|
|
||||||
'headers' => ['X-powered' => 'PHP'],
|
'headers' => ['X-powered' => 'PHP'],
|
||||||
'max_redirects' => 2,
|
'max_redirects' => 2,
|
||||||
'http_version' => '2.0',
|
'http_version' => '2.0',
|
||||||
'base_uri' => 'http://example.com',
|
|
||||||
'resolve' => ['localhost' => '127.0.0.1'],
|
'resolve' => ['localhost' => '127.0.0.1'],
|
||||||
'proxy' => 'proxy.org',
|
'proxy' => 'proxy.org',
|
||||||
'timeout' => 3.5,
|
'timeout' => 3.5,
|
||||||
|
@ -6,13 +6,11 @@ $container->loadFromExtension('framework', [
|
|||||||
'default_options' => [
|
'default_options' => [
|
||||||
'headers' => ['foo' => 'bar'],
|
'headers' => ['foo' => 'bar'],
|
||||||
],
|
],
|
||||||
'clients' => [
|
'scoped_clients' => [
|
||||||
'foo' => [
|
'foo' => [
|
||||||
'max_host_connections' => 5,
|
'base_uri' => 'http://example.com',
|
||||||
'default_options' => [
|
|
||||||
'headers' => ['bar' => 'baz'],
|
'headers' => ['bar' => 'baz'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
<framework:config>
|
<framework:config>
|
||||||
<framework:http-client max-host-connections="4">
|
<framework:http-client max-host-connections="4">
|
||||||
<framework:default-options />
|
<framework:default-options />
|
||||||
<framework:client name="foo">
|
<framework:scoped-client
|
||||||
<framework:default-options />
|
name="foo"
|
||||||
</framework:client>
|
base-uri="http://example.com"
|
||||||
|
/>
|
||||||
</framework:http-client>
|
</framework:http-client>
|
||||||
</framework:config>
|
</framework:config>
|
||||||
</container>
|
</container>
|
||||||
|
@ -12,10 +12,8 @@
|
|||||||
bindto="127.0.0.1"
|
bindto="127.0.0.1"
|
||||||
timeout="3.5"
|
timeout="3.5"
|
||||||
verify-peer="true"
|
verify-peer="true"
|
||||||
auth-basic="foo:bar"
|
|
||||||
max-redirects="2"
|
max-redirects="2"
|
||||||
http-version="2.0"
|
http-version="2.0"
|
||||||
base-uri="http://example.com"
|
|
||||||
verify-host="true"
|
verify-host="true"
|
||||||
cafile="/etc/ssl/cafile"
|
cafile="/etc/ssl/cafile"
|
||||||
capath="/etc/ssl"
|
capath="/etc/ssl"
|
||||||
@ -24,8 +22,6 @@
|
|||||||
passphrase="password123456"
|
passphrase="password123456"
|
||||||
ciphers="RC4-SHA:TLS13-AES-128-GCM-SHA256"
|
ciphers="RC4-SHA:TLS13-AES-128-GCM-SHA256"
|
||||||
>
|
>
|
||||||
<framework:query key="foo">bar</framework:query>
|
|
||||||
<framework:query key="bar">baz</framework:query>
|
|
||||||
<framework:header name="X-powered">PHP</framework:header>
|
<framework:header name="X-powered">PHP</framework:header>
|
||||||
<framework:resolve host="localhost">127.0.0.1</framework:resolve>
|
<framework:resolve host="localhost">127.0.0.1</framework:resolve>
|
||||||
<framework:peer-fingerprint>
|
<framework:peer-fingerprint>
|
||||||
|
@ -10,11 +10,9 @@
|
|||||||
<framework:default-options>
|
<framework:default-options>
|
||||||
<framework:header name="foo">bar</framework:header>
|
<framework:header name="foo">bar</framework:header>
|
||||||
</framework:default-options>
|
</framework:default-options>
|
||||||
<framework:client name="foo" max-host-connections="5">
|
<framework:scoped-client name="foo" base-uri="http://example.com">
|
||||||
<framework:default-options>
|
|
||||||
<framework:header name="bar">baz</framework:header>
|
<framework:header name="bar">baz</framework:header>
|
||||||
</framework:default-options>
|
</framework:scoped-client>
|
||||||
</framework:client>
|
|
||||||
</framework:http-client>
|
</framework:http-client>
|
||||||
</framework:config>
|
</framework:config>
|
||||||
</container>
|
</container>
|
||||||
|
@ -2,6 +2,6 @@ framework:
|
|||||||
http_client:
|
http_client:
|
||||||
max_host_connections: 4
|
max_host_connections: 4
|
||||||
default_options: ~
|
default_options: ~
|
||||||
clients:
|
scoped_clients:
|
||||||
foo:
|
foo:
|
||||||
default_options: ~
|
base_uri: http://example.com
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
framework:
|
framework:
|
||||||
http_client:
|
http_client:
|
||||||
default_options:
|
default_options:
|
||||||
auth_basic: foo:bar
|
|
||||||
query: {'foo': 'bar', 'bar': 'baz'}
|
|
||||||
headers:
|
headers:
|
||||||
X-powered: PHP
|
X-powered: PHP
|
||||||
max_redirects: 2
|
max_redirects: 2
|
||||||
http_version: 2.0
|
http_version: 2.0
|
||||||
base_uri: 'http://example.com'
|
|
||||||
resolve: {'localhost': '127.0.0.1'}
|
resolve: {'localhost': '127.0.0.1'}
|
||||||
proxy: proxy.org
|
proxy: proxy.org
|
||||||
timeout: 3.5
|
timeout: 3.5
|
||||||
|
@ -3,8 +3,7 @@ framework:
|
|||||||
max_host_connections: 4
|
max_host_connections: 4
|
||||||
default_options:
|
default_options:
|
||||||
headers: {'foo': 'bar'}
|
headers: {'foo': 'bar'}
|
||||||
clients:
|
scoped_clients:
|
||||||
foo:
|
foo:
|
||||||
max_host_connections: 5
|
base_uri: http://example.com
|
||||||
default_options:
|
|
||||||
headers: {'bar': 'baz'}
|
headers: {'bar': 'baz'}
|
||||||
|
@ -35,6 +35,7 @@ use Symfony\Component\DependencyInjection\Loader\ClosureLoader;
|
|||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||||
use Symfony\Component\DependencyInjection\Reference;
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpClient\ScopingHttpClient;
|
||||||
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
|
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
|
||||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||||
use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage;
|
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\Mapping\Loader\PropertyInfoLoader;
|
||||||
use Symfony\Component\Validator\Validation;
|
use Symfony\Component\Validator\Validation;
|
||||||
use Symfony\Component\Workflow;
|
use Symfony\Component\Workflow;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
||||||
|
|
||||||
abstract class FrameworkExtensionTest extends TestCase
|
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');
|
$this->assertTrue($container->hasDefinition('http_client'), '->registerHttpClientConfiguration() loads http_client.xml');
|
||||||
|
|
||||||
$defaultOptions = [
|
$defaultOptions = [
|
||||||
'query' => [],
|
|
||||||
'headers' => [],
|
'headers' => [],
|
||||||
'resolve' => [],
|
'resolve' => [],
|
||||||
];
|
];
|
||||||
$this->assertSame([$defaultOptions, 4], $container->getDefinition('http_client')->getArguments());
|
$this->assertSame([$defaultOptions, 4], $container->getDefinition('http_client')->getArguments());
|
||||||
|
|
||||||
$this->assertTrue($container->hasDefinition('foo'), 'should have the "foo" service.');
|
$this->assertTrue($container->hasDefinition('foo'), 'should have the "foo" service.');
|
||||||
$this->assertSame(HttpClientInterface::class, $container->getDefinition('foo')->getClass());
|
$this->assertSame(ScopingHttpClient::class, $container->getDefinition('foo')->getClass());
|
||||||
$this->assertSame([$defaultOptions, 4], $container->getDefinition('foo')->getArguments());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHttpClientOverrideDefaultOptions()
|
public function testHttpClientOverrideDefaultOptions()
|
||||||
{
|
{
|
||||||
$container = $this->createContainerFromFile('http_client_override_default_options');
|
$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(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()
|
public function testHttpClientFullDefaultOptions()
|
||||||
@ -1433,12 +1442,9 @@ abstract class FrameworkExtensionTest extends TestCase
|
|||||||
|
|
||||||
$defaultOptions = $container->getDefinition('http_client')->getArgument(0);
|
$defaultOptions = $container->getDefinition('http_client')->getArgument(0);
|
||||||
|
|
||||||
$this->assertSame('foo:bar', $defaultOptions['auth_basic']);
|
$this->assertSame(['X-powered' => 'PHP'], $defaultOptions['headers']);
|
||||||
$this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $defaultOptions['query']);
|
|
||||||
$this->assertSame(['x-powered' => ['PHP']], $defaultOptions['headers']);
|
|
||||||
$this->assertSame(2, $defaultOptions['max_redirects']);
|
$this->assertSame(2, $defaultOptions['max_redirects']);
|
||||||
$this->assertSame(2.0, (float) $defaultOptions['http_version']);
|
$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(['localhost' => '127.0.0.1'], $defaultOptions['resolve']);
|
||||||
$this->assertSame('proxy.org', $defaultOptions['proxy']);
|
$this->assertSame('proxy.org', $defaultOptions['proxy']);
|
||||||
$this->assertSame(3.5, $defaultOptions['timeout']);
|
$this->assertSame(3.5, $defaultOptions['timeout']);
|
||||||
|
@ -240,7 +240,7 @@ class MockResponse implements ResponseInterface
|
|||||||
$info = $mock->getInfo() ?: [];
|
$info = $mock->getInfo() ?: [];
|
||||||
$response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode(false) ?: 200;
|
$response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode(false) ?: 200;
|
||||||
$response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
|
$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 = [
|
$response->info = [
|
||||||
'start_time' => $response->info['start_time'],
|
'start_time' => $response->info['start_time'],
|
||||||
@ -282,7 +282,7 @@ class MockResponse implements ResponseInterface
|
|||||||
// "notify" completion
|
// "notify" completion
|
||||||
$onProgress($offset, $dlSize, $response->info);
|
$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));
|
throw new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $dlSize - $offset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,8 +122,7 @@ class ResponseHeaderBag extends HeaderBag
|
|||||||
parent::set($key, $values, $replace);
|
parent::set($key, $values, $replace);
|
||||||
|
|
||||||
// ensure the cache-control header has sensible defaults
|
// ensure the cache-control header has sensible defaults
|
||||||
if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true)) {
|
if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) {
|
||||||
$computed = $this->computeCacheControlValue();
|
|
||||||
$this->headers['cache-control'] = [$computed];
|
$this->headers['cache-control'] = [$computed];
|
||||||
$this->headerNames['cache-control'] = 'Cache-Control';
|
$this->headerNames['cache-control'] = 'Cache-Control';
|
||||||
$this->computedCacheControl = $this->parseCacheControl($computed);
|
$this->computedCacheControl = $this->parseCacheControl($computed);
|
||||||
|
@ -16,6 +16,7 @@ use Psr\Log\NullLogger;
|
|||||||
use Symfony\Component\HttpClient\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
use Symfony\Component\Mime\Part\AbstractPart;
|
use Symfony\Component\Mime\Part\AbstractPart;
|
||||||
use Symfony\Component\Mime\Part\DataPart;
|
use Symfony\Component\Mime\Part\DataPart;
|
||||||
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
|
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()));
|
$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
|
private function getBody(Request $request): ?AbstractPart
|
||||||
|
Reference in New Issue
Block a user