feature #38026 [HttpClient] Allow to provide additional curl options to CurlHttpClient (pizzaminded)
This PR was squashed before being merged into the 5.2-dev branch.
Discussion
----------
[HttpClient] Allow to provide additional curl options to CurlHttpClient
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Tickets | Fix #37798
| License | MIT
| Doc PR | symfony/symfony-docs#14195
~~**Tagging as a draft because:**~~
- ~~there is no Doc PR Ready yet~~
- probably there are better test cases required here.
This PR introduces an option to override default curl options defined in `CurlHttpClient`. To override them, there is a special place in `$options` provided:
````php
$response = $httpClient->request('GET', 'http://your.url.here', [
'extra' => [
'curl' => [
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
]
]
]);
````
This feature is available only in `CurlHttpClient` and would be ignored in another clients.
Commits
-------
89329bd979
[HttpClient] Allow to provide additional curl options to CurlHttpClient
This commit is contained in:
commit
0ceafbcbea
@ -9,6 +9,7 @@ CHANGELOG
|
||||
* added `StreamableInterface` to ease turning responses into PHP streams
|
||||
* added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
|
||||
* added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
|
||||
* added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
@ -40,6 +40,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
|
||||
private $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
|
||||
'extra' => [
|
||||
'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
@ -274,6 +277,11 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
|
||||
$curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
|
||||
}
|
||||
|
||||
if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
|
||||
$this->validateExtraCurlOptions($options['extra']['curl']);
|
||||
$curlopts += $options['extra']['curl'];
|
||||
}
|
||||
|
||||
if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
|
||||
unset($this->multi->pushedResponses[$url]);
|
||||
|
||||
@ -297,11 +305,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
|
||||
|
||||
foreach ($curlopts as $opt => $value) {
|
||||
if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt) {
|
||||
$constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
|
||||
return $v === $opt && 'C' === $k[0] && (0 === strpos($k, 'CURLOPT_') || 0 === strpos($k, 'CURLINFO_'));
|
||||
}, \ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
throw new TransportException(sprintf('Curl option "%s" is not supported.', key($constants) ?? $opt));
|
||||
$constantName = $this->findConstantName($opt);
|
||||
throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
|
||||
}
|
||||
}
|
||||
|
||||
@ -487,4 +492,101 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
|
||||
return implode('', self::resolveUrl($location, $url));
|
||||
};
|
||||
}
|
||||
|
||||
private function findConstantName($opt): ?string
|
||||
{
|
||||
$constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
|
||||
return $v === $opt && 'C' === $k[0] && (0 === strpos($k, 'CURLOPT_') || 0 === strpos($k, 'CURLINFO_'));
|
||||
}, \ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
return key($constants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents overriding options that are set internally throughout the request.
|
||||
*/
|
||||
private function validateExtraCurlOptions(array $options): void
|
||||
{
|
||||
$curloptsToConfig = [
|
||||
//options used in CurlHttpClient
|
||||
\CURLOPT_HTTPAUTH => 'auth_ntlm',
|
||||
\CURLOPT_USERPWD => 'auth_ntlm',
|
||||
\CURLOPT_RESOLVE => 'resolve',
|
||||
\CURLOPT_NOSIGNAL => 'timeout',
|
||||
\CURLOPT_HTTPHEADER => 'headers',
|
||||
\CURLOPT_INFILE => 'body',
|
||||
\CURLOPT_READFUNCTION => 'body',
|
||||
\CURLOPT_INFILESIZE => 'body',
|
||||
\CURLOPT_POSTFIELDS => 'body',
|
||||
\CURLOPT_UPLOAD => 'body',
|
||||
\CURLOPT_PINNEDPUBLICKEY => 'peer_fingerprint',
|
||||
\CURLOPT_UNIX_SOCKET_PATH => 'bindto',
|
||||
\CURLOPT_INTERFACE => 'bindto',
|
||||
\CURLOPT_TIMEOUT_MS => 'max_duration',
|
||||
\CURLOPT_TIMEOUT => 'max_duration',
|
||||
\CURLOPT_MAXREDIRS => 'max_redirects',
|
||||
\CURLOPT_PROXY => 'proxy',
|
||||
\CURLOPT_NOPROXY => 'no_proxy',
|
||||
\CURLOPT_SSL_VERIFYPEER => 'verify_peer',
|
||||
\CURLOPT_SSL_VERIFYHOST => 'verify_host',
|
||||
\CURLOPT_CAINFO => 'cafile',
|
||||
\CURLOPT_CAPATH => 'capath',
|
||||
\CURLOPT_SSL_CIPHER_LIST => 'ciphers',
|
||||
\CURLOPT_SSLCERT => 'local_cert',
|
||||
\CURLOPT_SSLKEY => 'local_pk',
|
||||
\CURLOPT_KEYPASSWD => 'passphrase',
|
||||
\CURLOPT_CERTINFO => 'capture_peer_cert_chain',
|
||||
\CURLOPT_USERAGENT => 'normalized_headers',
|
||||
\CURLOPT_REFERER => 'headers',
|
||||
//options used in CurlResponse
|
||||
\CURLOPT_NOPROGRESS => 'on_progress',
|
||||
\CURLOPT_PROGRESSFUNCTION => 'on_progress',
|
||||
];
|
||||
|
||||
$curloptsToCheck = [
|
||||
\CURLOPT_PRIVATE,
|
||||
\CURLOPT_HEADERFUNCTION,
|
||||
\CURLOPT_WRITEFUNCTION,
|
||||
\CURLOPT_VERBOSE,
|
||||
\CURLOPT_STDERR,
|
||||
\CURLOPT_RETURNTRANSFER,
|
||||
\CURLOPT_URL,
|
||||
\CURLOPT_FOLLOWLOCATION,
|
||||
\CURLOPT_HEADER,
|
||||
\CURLOPT_CONNECTTIMEOUT,
|
||||
\CURLOPT_CONNECTTIMEOUT_MS,
|
||||
\CURLOPT_HEADEROPT,
|
||||
\CURLOPT_HTTP_VERSION,
|
||||
\CURLOPT_PORT,
|
||||
\CURLOPT_DNS_USE_GLOBAL_CACHE,
|
||||
\CURLOPT_PROTOCOLS,
|
||||
\CURLOPT_REDIR_PROTOCOLS,
|
||||
\CURLOPT_COOKIEFILE,
|
||||
\CURLINFO_REDIRECT_COUNT,
|
||||
];
|
||||
|
||||
$methodOpts = [
|
||||
\CURLOPT_POST,
|
||||
\CURLOPT_PUT,
|
||||
\CURLOPT_CUSTOMREQUEST,
|
||||
\CURLOPT_HTTPGET,
|
||||
\CURLOPT_NOBODY,
|
||||
];
|
||||
|
||||
foreach ($options as $opt => $optValue) {
|
||||
if (isset($curloptsToConfig[$opt])) {
|
||||
$constName = $this->findConstantName($opt) ?? $opt;
|
||||
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
|
||||
}
|
||||
|
||||
if (\in_array($opt, $methodOpts)) {
|
||||
throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
|
||||
}
|
||||
|
||||
if (\in_array($opt, $curloptsToCheck)) {
|
||||
$constName = $this->findConstantName($opt) ?? $opt;
|
||||
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\HttpClient\Tests;
|
||||
|
||||
use Symfony\Component\HttpClient\CurlHttpClient;
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
@ -42,4 +43,49 @@ class CurlHttpClientTest extends HttpClientTestCase
|
||||
|
||||
parent::testTimeoutIsNotAFatalError();
|
||||
}
|
||||
|
||||
public function testOverridingRefererUsingCurlOptions()
|
||||
{
|
||||
$httpClient = $this->getHttpClient(__FUNCTION__);
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set "CURLOPT_REFERER" with "extra.curl", use option "headers" instead.');
|
||||
|
||||
$httpClient->request('GET', 'http://localhost:8057/', [
|
||||
'extra' => [
|
||||
'curl' => [
|
||||
\CURLOPT_REFERER => 'Banana',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testOverridingHttpMethodUsingCurlOptions()
|
||||
{
|
||||
$httpClient = $this->getHttpClient(__FUNCTION__);
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The HTTP method cannot be overridden using "extra.curl".');
|
||||
|
||||
$httpClient->request('POST', 'http://localhost:8057/', [
|
||||
'extra' => [
|
||||
'curl' => [
|
||||
\CURLOPT_HTTPGET => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testOverridingInternalAttributesUsingCurlOptions()
|
||||
{
|
||||
$httpClient = $this->getHttpClient(__FUNCTION__);
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set "CURLOPT_PRIVATE" with "extra.curl".');
|
||||
|
||||
$httpClient->request('POST', 'http://localhost:8057/', [
|
||||
'extra' => [
|
||||
'curl' => [
|
||||
\CURLOPT_PRIVATE => 'overriden private',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user