Skip to content

Commit 98076b0

Browse files
[HttpClient] Remove support for HTTP/2
1 parent 009fb4e commit 98076b0

File tree

9 files changed

+33
-503
lines changed

9 files changed

+33
-503
lines changed

src/Symfony/Component/HttpClient/CurlHttpClient.php

Lines changed: 7 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
1717
use Symfony\Component\HttpClient\Exception\TransportException;
1818
use Symfony\Component\HttpClient\Internal\CurlClientState;
19-
use Symfony\Component\HttpClient\Internal\PushedResponse;
2019
use Symfony\Component\HttpClient\Response\CurlResponse;
2120
use Symfony\Component\HttpClient\Response\ResponseStream;
2221
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -27,8 +26,7 @@
2726
/**
2827
* A performant implementation of the HttpClientInterface contracts based on the curl extension.
2928
*
30-
* This provides fully concurrent HTTP requests, with transparent
31-
* HTTP/2 push when a curl version that supports it is installed.
29+
* This provides fully concurrent HTTP requests and support for HTTP/2.
3230
*
3331
* @author Nicolas Grekas <p@tchwork.com>
3432
*/
@@ -52,7 +50,6 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
5250
private ?LoggerInterface $logger = null;
5351

5452
private int $maxHostConnections;
55-
private int $maxPendingPushes;
5653

5754
/**
5855
* An internal object to share state between the client and its responses.
@@ -62,19 +59,16 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
6259
/**
6360
* @param array $defaultOptions Default request's options
6461
* @param int $maxHostConnections The maximum number of connections to a single host
65-
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
6662
*
6763
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
6864
*/
69-
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 0)
65+
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
7066
{
7167
if (!\extension_loaded('curl')) {
7268
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
7369
}
7470

7571
$this->maxHostConnections = $maxHostConnections;
76-
$this->maxPendingPushes = $maxPendingPushes;
77-
7872
$this->defaultOptions['buffer'] ??= self::shouldBuffer(...);
7973

8074
if ($defaultOptions) {
@@ -299,27 +293,9 @@ public function request(string $method, string $url, array $options = []): Respo
299293
$curlopts += $options['extra']['curl'];
300294
}
301295

302-
if ($pushedResponse = $multi->pushedResponses[$url] ?? null) {
303-
unset($multi->pushedResponses[$url]);
304-
305-
if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
306-
$this->logger?->debug(\sprintf('Accepting pushed response: "%s %s"', $method, $url));
307-
308-
// Reinitialize the pushed response with request's options
309-
$ch = $pushedResponse->handle;
310-
$pushedResponse = $pushedResponse->response;
311-
$pushedResponse->__construct($multi, $url, $options, $this->logger);
312-
} else {
313-
$this->logger?->debug(\sprintf('Rejecting pushed response: "%s"', $url));
314-
$pushedResponse = null;
315-
}
316-
}
317-
318-
if (!$pushedResponse) {
319-
$ch = curl_init();
320-
$this->logger?->info(\sprintf('Request: "%s %s"', $method, $url));
321-
$curlopts += [\CURLOPT_SHARE => $multi->share];
322-
}
296+
$ch = curl_init();
297+
$this->logger?->info(\sprintf('Request: "%s %s"', $method, $url));
298+
$curlopts += [\CURLOPT_SHARE => $multi->share];
323299

324300
foreach ($curlopts as $opt => $value) {
325301
if (\PHP_INT_SIZE === 8 && \defined('CURLOPT_INFILESIZE_LARGE') && \CURLOPT_INFILESIZE === $opt && $value >= 1 << 31) {
@@ -331,7 +307,7 @@ public function request(string $method, string $url, array $options = []): Respo
331307
}
332308
}
333309

334-
return $pushedResponse ?? new CurlResponse($multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $authority), CurlClientState::$curlVersion['version_number'], $url);
310+
return new CurlResponse($multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $authority), CurlClientState::$curlVersion['version_number'], $url);
335311
}
336312

337313
public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface
@@ -358,35 +334,6 @@ public function reset(): void
358334
}
359335
}
360336

361-
/**
362-
* Accepts pushed responses only if their headers related to authentication match the request.
363-
*/
364-
private static function acceptPushForRequest(string $method, array $options, PushedResponse $pushedResponse): bool
365-
{
366-
if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
367-
return false;
368-
}
369-
370-
foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) {
371-
if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
372-
return false;
373-
}
374-
}
375-
376-
foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
377-
$normalizedHeaders = $options['normalized_headers'][$k] ?? [];
378-
foreach ($normalizedHeaders as $i => $v) {
379-
$normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
380-
}
381-
382-
if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
383-
return false;
384-
}
385-
}
386-
387-
return true;
388-
}
389-
390337
/**
391338
* Wraps the request's body callback to allow it to return strings longer than curl requested.
392339
*/
@@ -455,7 +402,7 @@ private static function createRedirectResolver(array $options, string $authority
455402
private function ensureState(): CurlClientState
456403
{
457404
if (!isset($this->multi)) {
458-
$this->multi = new CurlClientState($this->maxHostConnections, $this->maxPendingPushes);
405+
$this->multi = new CurlClientState($this->maxHostConnections);
459406
$this->multi->logger = $this->logger;
460407
}
461408

src/Symfony/Component/HttpClient/HttpClient.php

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,21 @@ final class HttpClient
2929
*
3030
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
3131
*/
32-
public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
32+
public static function create(array $defaultOptions = [], int $maxHostConnections = 6): HttpClientInterface
3333
{
34-
if ($amp = class_exists(AmpRequest::class) && (\PHP_VERSION_ID >= 80400 || !is_subclass_of(AmpRequest::class, HttpMessage::class))) {
35-
if (!\extension_loaded('curl')) {
36-
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
37-
}
38-
39-
// Skip curl when HTTP/2 push is unsupported or buggy, see https://bugs.php.net/77535
40-
if (!\defined('CURLMOPT_PUSHFUNCTION')) {
41-
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
42-
}
43-
44-
static $curlVersion = null;
45-
$curlVersion ??= curl_version();
46-
47-
// HTTP/2 push crashes before curl 7.61
48-
if (0x073D00 > $curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & $curlVersion['features'])) {
49-
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
50-
}
51-
}
52-
5334
if (\extension_loaded('curl')) {
5435
if ('\\' !== \DIRECTORY_SEPARATOR || isset($defaultOptions['cafile']) || isset($defaultOptions['capath']) || \ini_get('curl.cainfo') || \ini_get('openssl.cafile') || \ini_get('openssl.capath')) {
55-
return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
36+
return new CurlHttpClient($defaultOptions, $maxHostConnections);
5637
}
5738

5839
@trigger_error('Configure the "curl.cainfo", "openssl.cafile" or "openssl.capath" php.ini setting to enable the CurlHttpClient', \E_USER_WARNING);
5940
}
6041

61-
if ($amp) {
62-
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
42+
if (class_exists(AmpRequest::class) && (\PHP_VERSION_ID >= 80400 || !is_subclass_of(AmpRequest::class, HttpMessage::class))) {
43+
return new AmpHttpClient($defaultOptions, null, $maxHostConnections);
6344
}
6445

65-
@trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^5" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
46+
@trigger_error('Install the curl extension or run "composer require amphp/http-client:^5" to perform async HTTP operations, including HTTP/2 support', \E_USER_NOTICE);
6647

6748
return new NativeHttpClient($defaultOptions, $maxHostConnections);
6849
}

src/Symfony/Component/HttpClient/Internal/CurlClientState.php

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\HttpClient\Internal;
1313

1414
use Psr\Log\LoggerInterface;
15-
use Symfony\Component\HttpClient\Response\CurlResponse;
1615

1716
/**
1817
* Internal representation of the cURL client's state.
@@ -27,8 +26,6 @@ final class CurlClientState extends ClientState
2726
public ?\CurlShareHandle $share;
2827
public bool $performing = false;
2928

30-
/** @var PushedResponse[] */
31-
public array $pushedResponses = [];
3229
public DnsCache $dnsCache;
3330
/** @var float[] */
3431
public array $pauseExpiries = [];
@@ -37,7 +34,7 @@ final class CurlClientState extends ClientState
3734

3835
public static array $curlVersion;
3936

40-
public function __construct(int $maxHostConnections, int $maxPendingPushes)
37+
public function __construct(int $maxHostConnections)
4138
{
4239
self::$curlVersion ??= curl_version();
4340

@@ -55,37 +52,10 @@ public function __construct(int $maxHostConnections, int $maxPendingPushes)
5552
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
5653
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
5754
}
58-
59-
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
60-
if (0 >= $maxPendingPushes) {
61-
return;
62-
}
63-
64-
// HTTP/2 push crashes before curl 7.61
65-
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
66-
return;
67-
}
68-
69-
// Clone to prevent a circular reference
70-
$multi = clone $this;
71-
$multi->handle = null;
72-
$multi->share = null;
73-
$multi->pushedResponses = &$this->pushedResponses;
74-
$multi->logger = &$this->logger;
75-
$multi->handlesActivity = &$this->handlesActivity;
76-
$multi->openHandles = &$this->openHandles;
77-
78-
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, static fn ($parent, $pushed, array $requestHeaders) => $multi->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes));
7955
}
8056

8157
public function reset(): void
8258
{
83-
foreach ($this->pushedResponses as $url => $response) {
84-
$this->logger?->debug(\sprintf('Unused pushed response: "%s"', $url));
85-
curl_multi_remove_handle($this->handle, $response->handle);
86-
}
87-
88-
$this->pushedResponses = [];
8959
$this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
9060
$this->dnsCache->removals = $this->dnsCache->hostnames = [];
9161

@@ -98,46 +68,4 @@ public function reset(): void
9868
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
9969
}
10070
}
101-
102-
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int
103-
{
104-
$headers = [];
105-
$origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);
106-
107-
foreach ($requestHeaders as $h) {
108-
if (false !== $i = strpos($h, ':', 1)) {
109-
$headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
110-
}
111-
}
112-
113-
if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
114-
$this->logger?->debug(\sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));
115-
116-
return \CURL_PUSH_DENY;
117-
}
118-
119-
$url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
120-
121-
// curl before 7.65 doesn't validate the pushed ":authority" header,
122-
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
123-
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
124-
if (!str_starts_with($origin, $url.'/')) {
125-
$this->logger?->debug(\sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url));
126-
127-
return \CURL_PUSH_DENY;
128-
}
129-
130-
if ($maxPendingPushes <= \count($this->pushedResponses)) {
131-
$fifoUrl = key($this->pushedResponses);
132-
unset($this->pushedResponses[$fifoUrl]);
133-
$this->logger?->debug(\sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
134-
}
135-
136-
$url .= $headers[':path'][0];
137-
$this->logger?->debug(\sprintf('Queueing pushed response: "%s"', $url));
138-
139-
$this->pushedResponses[$url] = new PushedResponse(new CurlResponse($this, $pushed), $headers, $this->openHandles[(int) $parent][1] ?? [], $pushed);
140-
141-
return \CURL_PUSH_OK;
142-
}
14371
}

src/Symfony/Component/HttpClient/Internal/PushedResponse.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)