mirror of
https://github.com/symfony/symfony.git
synced 2026-03-24 00:32:15 +01:00
[HttpClient] Add support of the persistent cURL handles
This commit is contained in:
committed by
Nicolas Grekas
parent
bf7a2a5fc6
commit
bd9f6c7e36
@@ -29,6 +29,8 @@
|
||||
<errorLevel type="suppress">
|
||||
<!-- These classes have been added in PHP 8.4 -->
|
||||
<referencedClass name="BcMath\Number"/>
|
||||
<!-- These classes have been added in PHP 8.5 -->
|
||||
<referencedClass name="CurlSharePersistentHandle"/>
|
||||
</errorLevel>
|
||||
</UndefinedClass>
|
||||
<UnusedClass>
|
||||
@@ -49,6 +51,12 @@
|
||||
<directory name="src/Symfony" />
|
||||
</errorLevel>
|
||||
</UnusedConstructor>
|
||||
<UndefinedFunction>
|
||||
<errorLevel type="suppress">
|
||||
<!-- These functions have been added in PHP 8.5 -->
|
||||
<referencedFunction name="curl_share_init_persistent"/>
|
||||
</errorLevel>
|
||||
</UndefinedFunction>
|
||||
</issueHandlers>
|
||||
|
||||
<forbiddenFunctions>
|
||||
|
||||
@@ -5,6 +5,7 @@ CHANGELOG
|
||||
---
|
||||
|
||||
* Add support for the `max_connect_duration` option
|
||||
* Add option `extra.use_persistent_connections` to `CurlHttpClient` to control the use of persistent connections introduced in PHP 8.5
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
@@ -41,10 +41,13 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
|
||||
];
|
||||
|
||||
private array $defaultOptions = self::OPTIONS_DEFAULTS + [
|
||||
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
|
||||
// password as the second one; or string like username:password - enabling NTLM auth
|
||||
// array|string - an array containing the username as first value, and optionally the
|
||||
// password as the second one; or string like username:password - enabling NTLM auth
|
||||
'auth_ntlm' => null,
|
||||
'extra' => [
|
||||
'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
|
||||
'use_persistent_connections' => false,
|
||||
// A list of extra curl options indexed by their corresponding CURLOPT_*
|
||||
'curl' => [],
|
||||
],
|
||||
];
|
||||
private static array $emptyDefaults = self::OPTIONS_DEFAULTS + ['auth_ntlm' => null];
|
||||
@@ -313,7 +316,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
|
||||
if (!$pushedResponse) {
|
||||
$ch = curl_init();
|
||||
$this->logger?->info(\sprintf('Request: "%s %s"', $method, $url));
|
||||
$curlopts += [\CURLOPT_SHARE => $this->multi->share];
|
||||
$curlopts += [\CURLOPT_SHARE => ($options['extra']['use_persistent_connections'] ?? false) ? $this->multi->share : $this->multi->persistentShare];
|
||||
}
|
||||
|
||||
foreach ($curlopts as $opt => $value) {
|
||||
|
||||
@@ -25,6 +25,7 @@ final class CurlClientState extends ClientState
|
||||
{
|
||||
public ?\CurlMultiHandle $handle;
|
||||
public ?\CurlShareHandle $share;
|
||||
public \CurlShareHandle|\CurlSharePersistentHandle|null $persistentShare;
|
||||
public bool $performing = false;
|
||||
|
||||
/** @var PushedResponse[] */
|
||||
@@ -44,8 +45,8 @@ final class CurlClientState extends ClientState
|
||||
self::$curlVersion ??= curl_version();
|
||||
$this->dnsCache = new DnsCache();
|
||||
|
||||
// handle and share are initialized lazily in __get()
|
||||
unset($this->handle, $this->share);
|
||||
// handle, share and persistentShare are initialized lazily in __get()
|
||||
unset($this->handle, $this->share, $this->persistentShare);
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
@@ -64,15 +65,23 @@ final class CurlClientState extends ClientState
|
||||
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
if ('persistentShare' === $name) {
|
||||
if (\PHP_VERSION_ID < 80500) {
|
||||
return $this->persistentShare = $this->share;
|
||||
}
|
||||
|
||||
return $this->persistentShare = curl_share_init_persistent([
|
||||
\CURL_LOCK_DATA_DNS,
|
||||
\CURL_LOCK_DATA_SSL_SESSION,
|
||||
\CURL_LOCK_DATA_CONNECT,
|
||||
]);
|
||||
}
|
||||
|
||||
if ('share' === $name) {
|
||||
$this->share = curl_share_init();
|
||||
|
||||
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
|
||||
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
|
||||
|
||||
if (\defined('CURL_LOCK_DATA_CONNECT')) {
|
||||
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
|
||||
}
|
||||
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
|
||||
|
||||
return $this->share;
|
||||
}
|
||||
@@ -98,6 +107,7 @@ final class CurlClientState extends ClientState
|
||||
$multi = clone $this;
|
||||
$multi->handle = null;
|
||||
$multi->share = null;
|
||||
$multi->persistentShare = null;
|
||||
$multi->pushedResponses = &$this->pushedResponses;
|
||||
$multi->logger = &$this->logger;
|
||||
$multi->handlesActivity = &$this->handlesActivity;
|
||||
|
||||
@@ -22,15 +22,16 @@ class CurlHttpClientTest extends HttpClientTestCase
|
||||
{
|
||||
protected function getHttpClient(string $testCase): CurlHttpClient
|
||||
{
|
||||
$usePersistentConnections = str_contains($testCase, 'Persistent');
|
||||
if (!str_contains($testCase, 'Push')) {
|
||||
return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]);
|
||||
return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false, 'extra' => ['use_persistent_connections' => $usePersistentConnections]]);
|
||||
}
|
||||
|
||||
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > ($v = curl_version())['version_number'] || !(\CURL_VERSION_HTTP2 & $v['features'])) {
|
||||
$this->markTestSkipped('curl <7.61 is used or it is not compiled with support for HTTP/2 PUSH');
|
||||
}
|
||||
|
||||
return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false], 6, 50);
|
||||
return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false, 'extra' => ['use_persistent_connections' => $usePersistentConnections]], 6, 50);
|
||||
}
|
||||
|
||||
public function testTimeoutIsNotAFatalError()
|
||||
@@ -82,6 +83,24 @@ class CurlHttpClientTest extends HttpClientTestCase
|
||||
self::assertInstanceOf(\CurlShareHandle::class, $state->share);
|
||||
}
|
||||
|
||||
public function testCurlClientPersistentStateInitializesHandlesLazily()
|
||||
{
|
||||
$client = $this->getHttpClient(__FUNCTION__);
|
||||
|
||||
$r = new \ReflectionProperty($client, 'multi');
|
||||
$state = $r->getValue($client);
|
||||
|
||||
self::assertFalse(isset($state->handle));
|
||||
self::assertFalse(isset($state->share));
|
||||
self::assertFalse(isset($state->persistentShare));
|
||||
|
||||
$client->request('GET', 'http://127.0.0.1:8057/json')->getStatusCode();
|
||||
|
||||
self::assertInstanceOf(\CurlMultiHandle::class, $state->handle);
|
||||
self::assertInstanceOf(\CurlShareHandle::class, $state->share);
|
||||
self::assertInstanceOf(\PHP_VERSION_ID >= 80500 ? \CurlSharePersistentHandle::class : \CurlShareHandle::class, $state->persistentShare);
|
||||
}
|
||||
|
||||
public function testProcessAfterReset()
|
||||
{
|
||||
$client = $this->getHttpClient(__FUNCTION__);
|
||||
|
||||
@@ -24,41 +24,61 @@ use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
|
||||
interface HttpClientInterface
|
||||
{
|
||||
public const OPTIONS_DEFAULTS = [
|
||||
'auth_basic' => null, // array|string - an array containing the username as first value, and optionally the
|
||||
// password as the second one; or string like username:password - enabling HTTP Basic
|
||||
// authentication (RFC 7617)
|
||||
'auth_bearer' => null, // string - a token enabling HTTP Bearer authorization (RFC 6750)
|
||||
'query' => [], // string[] - associative array of query string values to merge with the request's URL
|
||||
'headers' => [], // iterable|string[]|string[][] - headers names provided as keys or as part of values
|
||||
'body' => '', // array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string
|
||||
// smaller than the amount requested as argument; the empty string signals EOF; if
|
||||
// an array is passed, it is meant as a form payload of field names and values
|
||||
'json' => null, // mixed - if set, implementations MUST set the "body" option to the JSON-encoded
|
||||
// value and set the "content-type" header to a JSON-compatible value if it is not
|
||||
// explicitly defined in the headers option - typically "application/json"
|
||||
'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that
|
||||
// MUST be available via $response->getInfo('user_data') - not used internally
|
||||
'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower than or equal to 0
|
||||
// means redirects should not be followed; "Authorization" and "Cookie" headers MUST
|
||||
// NOT follow except for the initial host name
|
||||
'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0
|
||||
'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2
|
||||
'buffer' => true, // bool|resource|\Closure - whether the content of the response should be buffered or not,
|
||||
// or a stream resource where the response body should be written,
|
||||
// or a closure telling if/where the response should be buffered based on its headers
|
||||
'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort the
|
||||
// request; it MUST be called on connection, on headers and on completion; it SHOULD be
|
||||
// called on upload/download of data and at least 1/s
|
||||
'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution
|
||||
'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
|
||||
'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached
|
||||
'timeout' => null, // float - the idle timeout (in seconds) - defaults to ini_get('default_socket_timeout')
|
||||
'max_duration' => 0, // float - the maximum execution time (in seconds) for the request+response as a whole;
|
||||
// a value lower than or equal to 0 means it is unlimited
|
||||
'max_connect_duration' => 0, // float - the maximum duration (in seconds) allowed for DNS + TCP + TLS connection;
|
||||
// a value lower than or equal to 0 means unlimited, as long as option timeout is respected
|
||||
'bindto' => '0', // string - the interface or the local socket to bind to
|
||||
'verify_peer' => true, // see https://php.net/context.ssl for the following options
|
||||
// array|string - an array containing the username as first value, and optionally the
|
||||
// password as the second one; or string like username:password - enabling HTTP Basic
|
||||
// authentication (RFC 7617)
|
||||
'auth_basic' => null,
|
||||
// string - a token enabling HTTP Bearer authorization (RFC 6750)
|
||||
'auth_bearer' => null,
|
||||
// string[] - associative array of query string values to merge with the request's URL
|
||||
'query' => [],
|
||||
// iterable|string[]|string[][] - headers names provided as keys or as part of values
|
||||
'headers' => [],
|
||||
// array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string
|
||||
// smaller than the amount requested as argument; the empty string signals EOF; if
|
||||
// an array is passed, it is meant as a form payload of field names and values
|
||||
'body' => '',
|
||||
// mixed - if set, implementations MUST set the "body" option to the JSON-encoded
|
||||
// value and set the "content-type" header to a JSON-compatible value if it is not
|
||||
// explicitly defined in the headers option - typically "application/json"
|
||||
'json' => null,
|
||||
// mixed - any extra data to attach to the request (scalar, callable, object...) that
|
||||
// MUST be available via $response->getInfo('user_data') - not used internally
|
||||
'user_data' => null,
|
||||
// int - the maximum number of redirects to follow; a value lower than or equal to 0
|
||||
// means redirects should not be followed; "Authorization" and "Cookie" headers MUST
|
||||
// NOT follow except for the initial host name
|
||||
'max_redirects' => 20,
|
||||
// string - defaults to the best supported version, typically 1.1 or 2.0
|
||||
'http_version' => null,
|
||||
// string - the URI to resolve relative URLs, following rules in RFC 3986, section 2
|
||||
'base_uri' => null,
|
||||
// bool|resource|\Closure - whether the content of the response should be buffered or not,
|
||||
// or a stream resource where the response body should be written,
|
||||
// or a closure telling if/where the response should be buffered based on its headers
|
||||
'buffer' => true,
|
||||
// callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort the
|
||||
// request; it MUST be called on connection, on headers and on completion; it SHOULD be
|
||||
// called on upload/download of data and at least 1/s
|
||||
'on_progress' => null,
|
||||
// string[] - a map of host to IP address that SHOULD replace DNS resolution
|
||||
'resolve' => [],
|
||||
// string - by default, the proxy-related env vars handled by curl SHOULD be honored
|
||||
'proxy' => null,
|
||||
// string - a comma separated list of hosts that do not require a proxy to be reached
|
||||
'no_proxy' => null,
|
||||
// float - the idle timeout (in seconds) - defaults to ini_get('default_socket_timeout')
|
||||
'timeout' => null,
|
||||
// float - the maximum execution time (in seconds) for the request+response as a whole;
|
||||
// a value lower than or equal to 0 means it is unlimited
|
||||
'max_duration' => 0,
|
||||
// float - the maximum duration (in seconds) allowed for DNS + TCP + TLS connection;
|
||||
// a value lower than or equal to 0 means unlimited, as long as option timeout is respected
|
||||
'max_connect_duration' => 0,
|
||||
// string - the interface or the local socket to bind to
|
||||
'bindto' => '0',
|
||||
// see https://php.net/context.ssl for the following options
|
||||
'verify_peer' => true,
|
||||
'verify_host' => true,
|
||||
'cafile' => null,
|
||||
'capath' => null,
|
||||
@@ -68,8 +88,13 @@ interface HttpClientInterface
|
||||
'ciphers' => null,
|
||||
'peer_fingerprint' => null,
|
||||
'capture_peer_cert_chain' => false,
|
||||
'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, // STREAM_CRYPTO_METHOD_TLSv*_CLIENT - minimum TLS version
|
||||
'extra' => [], // array - additional options that can be ignored if unsupported, unlike regular options
|
||||
// STREAM_CRYPTO_METHOD_TLSv*_CLIENT - minimum TLS version
|
||||
'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
|
||||
// array - additional options that can be ignored if unsupported, unlike regular options
|
||||
'extra' => [
|
||||
// bool - whether to use persistent connections where supported
|
||||
'use_persistent_connections' => false,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user