From f2d21a7377eeb5fca124fa35e51680a01ff68760 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2025 16:08:14 +0200 Subject: [PATCH 1/9] Allow Symfony ^8.0 --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 7ca008f..3fffa2e 100644 --- a/composer.json +++ b/composer.json @@ -37,12 +37,12 @@ "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "conflict": { "amphp/amp": "<2.5", From bf4e553a73ff7e61c49daeb7f73bd50256f95fde Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2025 17:50:55 +0200 Subject: [PATCH 2/9] Bump Symfony 8 to PHP >= 8.4 --- composer.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 3fffa2e..06e8e81 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "symfony/http-client-implementation": "3.0" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", @@ -37,17 +37,16 @@ "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" }, "conflict": { "amphp/amp": "<2.5", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" + "php-http/discovery": "<1.15" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpClient\\": "" }, From 7e246e2f2f8bc3aa1ccd0991a6295de57e13fdb7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 3 Jun 2025 17:41:25 +0200 Subject: [PATCH 3/9] Remove deadcode after the bump to PHP >= 8.4 --- AmpHttpClient.php | 4 ---- HttpClient.php | 2 +- NativeHttpClient.php | 9 +-------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/AmpHttpClient.php b/AmpHttpClient.php index 4c73fba..6bb41af 100644 --- a/AmpHttpClient.php +++ b/AmpHttpClient.php @@ -36,10 +36,6 @@ throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^4.2.1".'); } -if (\PHP_VERSION_ID < 80400 && is_subclass_of(Request::class, HttpMessage::class)) { - throw new \LogicException('Using "Symfony\Component\HttpClient\AmpHttpClient" with amphp/http-client >= 5 requires PHP >= 8.4. Try running "composer require amphp/http-client:^4.2.1" or upgrade to PHP >= 8.4.'); -} - /** * A portable implementation of the HttpClientInterface contracts based on Amp's HTTP client. * diff --git a/HttpClient.php b/HttpClient.php index 3eb3665..7bd7068 100644 --- a/HttpClient.php +++ b/HttpClient.php @@ -31,7 +31,7 @@ final class HttpClient */ public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface { - if ($amp = class_exists(AmpRequest::class) && (\PHP_VERSION_ID >= 80400 || !is_subclass_of(AmpRequest::class, HttpMessage::class))) { + if ($amp = class_exists(AmpRequest::class)) { if (!\extension_loaded('curl')) { return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes); } diff --git a/NativeHttpClient.php b/NativeHttpClient.php index 941d375..0022d58 100644 --- a/NativeHttpClient.php +++ b/NativeHttpClient.php @@ -80,9 +80,6 @@ public function request(string $method, string $url, array $options = []): Respo if (str_starts_with($options['bindto'], 'host!')) { $options['bindto'] = substr($options['bindto'], 5); } - if ((\PHP_VERSION_ID < 80223 || 80300 <= \PHP_VERSION_ID && 80311 < \PHP_VERSION_ID) && '\\' === \DIRECTORY_SEPARATOR && '[' === $options['bindto'][0]) { - $options['bindto'] = preg_replace('{^\[[^\]]++\]}', '[$0]', $options['bindto']); - } } $hasContentLength = isset($options['normalized_headers']['content-length']); @@ -429,11 +426,7 @@ private static function createRedirectResolver(array $options, string $authority $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders); $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders); - if (\PHP_VERSION_ID >= 80300) { - stream_context_set_options($context, ['http' => $options]); - } else { - stream_context_set_option($context, ['http' => $options]); - } + stream_context_set_options($context, ['http' => $options]); } } From 9115a4dfe8a2eb319a0bbf9fc02354cc8b0c92a0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Jun 2025 11:54:04 +0200 Subject: [PATCH 4/9] [HttpClient] Deprecate using amphp/http-client < 5 --- AmpHttpClient.php | 3 +++ CHANGELOG.md | 5 +++++ HttpClient.php | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/AmpHttpClient.php b/AmpHttpClient.php index 4c73fba..1420ed2 100644 --- a/AmpHttpClient.php +++ b/AmpHttpClient.php @@ -78,6 +78,9 @@ public function __construct(array $defaultOptions = [], ?callable $clientConfigu if (is_subclass_of(Request::class, HttpMessage::class)) { $this->multi = new AmpClientStateV5($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger); } else { + if (\PHP_VERSION_ID >= 80400) { + trigger_deprecation('symfony/http-client', '7.4', 'Using amphp/http-client < 5 is deprecated. Try running "composer require amphp/http-client:^5".'); + } $this->multi = new AmpClientStateV4($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger); } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 40dc2ec..8a44989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * Deprecate using amphp/http-client < 5 + 7.3 --- diff --git a/HttpClient.php b/HttpClient.php index 3eb3665..2765935 100644 --- a/HttpClient.php +++ b/HttpClient.php @@ -62,7 +62,7 @@ public static function create(array $defaultOptions = [], int $maxHostConnection return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes); } - @trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^4.2.1" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE); + @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); return new NativeHttpClient($defaultOptions, $maxHostConnections); } From 129fc46ad6e880438cd4989624acec0cca4fb235 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Jun 2025 17:01:05 +0200 Subject: [PATCH 5/9] Fix leftover --- AmpHttpClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmpHttpClient.php b/AmpHttpClient.php index 1420ed2..b45229f 100644 --- a/AmpHttpClient.php +++ b/AmpHttpClient.php @@ -33,7 +33,7 @@ use Symfony\Contracts\Service\ResetInterface; if (!interface_exists(DelegateHttpClient::class)) { - throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^4.2.1".'); + throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^5".'); } if (\PHP_VERSION_ID < 80400 && is_subclass_of(Request::class, HttpMessage::class)) { From 862d1b5788c0357e064d8548830420103a0e120a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Jun 2025 16:59:51 +0200 Subject: [PATCH 6/9] [HttpClient] Remove support for amphp/http-client < 5 --- AmpHttpClient.php | 42 +--- CHANGELOG.md | 5 + Internal/AmpBodyV4.php | 148 ----------- Internal/AmpClientStateV4.php | 215 ---------------- Internal/AmpListenerV4.php | 184 -------------- Internal/AmpResolverV4.php | 62 ----- Response/AmpResponseV4.php | 458 ---------------------------------- composer.json | 8 +- 8 files changed, 17 insertions(+), 1105 deletions(-) delete mode 100644 Internal/AmpBodyV4.php delete mode 100644 Internal/AmpClientStateV4.php delete mode 100644 Internal/AmpListenerV4.php delete mode 100644 Internal/AmpResolverV4.php delete mode 100644 Response/AmpResponseV4.php diff --git a/AmpHttpClient.php b/AmpHttpClient.php index 90c8125..cc858df 100644 --- a/AmpHttpClient.php +++ b/AmpHttpClient.php @@ -12,19 +12,15 @@ namespace Symfony\Component\HttpClient; use Amp\CancelledException; -use Amp\DeferredFuture; use Amp\Http\Client\DelegateHttpClient; use Amp\Http\Client\InterceptedHttpClient; use Amp\Http\Client\PooledHttpClient; use Amp\Http\Client\Request; -use Amp\Http\HttpMessage; use Amp\Http\Tunnel\Http1TunnelConnector; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\HttpClient\Exception\TransportException; -use Symfony\Component\HttpClient\Internal\AmpClientStateV4; use Symfony\Component\HttpClient\Internal\AmpClientStateV5; -use Symfony\Component\HttpClient\Response\AmpResponseV4; use Symfony\Component\HttpClient\Response\AmpResponseV5; use Symfony\Component\HttpClient\Response\ResponseStream; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -52,7 +48,7 @@ final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, private array $defaultOptions = self::OPTIONS_DEFAULTS; private static array $emptyDefaults = self::OPTIONS_DEFAULTS; - private AmpClientStateV4|AmpClientStateV5 $multi; + private AmpClientStateV5 $multi; /** * @param array $defaultOptions Default requests' options @@ -71,14 +67,7 @@ public function __construct(array $defaultOptions = [], ?callable $clientConfigu [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); } - if (is_subclass_of(Request::class, HttpMessage::class)) { - $this->multi = new AmpClientStateV5($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger); - } else { - if (\PHP_VERSION_ID >= 80400) { - trigger_deprecation('symfony/http-client', '7.4', 'Using amphp/http-client < 5 is deprecated. Try running "composer require amphp/http-client:^5".'); - } - $this->multi = new AmpClientStateV4($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger); - } + $this->multi = new AmpClientStateV5($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger); } /** @@ -139,10 +128,9 @@ public function request(string $method, string $url, array $options = []): Respo $request->addHeader($h[0], $h[1]); } - $coef = $request instanceof HttpMessage ? 1 : 1000; - $request->setTcpConnectTimeout($coef * $options['timeout']); - $request->setTlsHandshakeTimeout($coef * $options['timeout']); - $request->setTransferTimeout($coef * $options['max_duration']); + $request->setTcpConnectTimeout($options['timeout']); + $request->setTlsHandshakeTimeout($options['timeout']); + $request->setTransferTimeout($options['max_duration']); if (method_exists($request, 'setInactivityTimeout')) { $request->setInactivityTimeout(0); } @@ -153,24 +141,16 @@ public function request(string $method, string $url, array $options = []): Respo $request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth))); } - if ($request instanceof HttpMessage) { - return new AmpResponseV5($this->multi, $request, $options, $this->logger); - } - - return new AmpResponseV4($this->multi, $request, $options, $this->logger); + return new AmpResponseV5($this->multi, $request, $options, $this->logger); } public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { - if ($responses instanceof AmpResponseV4 || $responses instanceof AmpResponseV5) { + if ($responses instanceof AmpResponseV5) { $responses = [$responses]; } - if ($this->multi instanceof AmpClientStateV5) { - return new ResponseStream(AmpResponseV5::stream($responses, $timeout)); - } - - return new ResponseStream(AmpResponseV4::stream($responses, $timeout)); + return new ResponseStream(AmpResponseV5::stream($responses, $timeout)); } public function reset(): void @@ -179,11 +159,7 @@ public function reset(): void foreach ($this->multi->pushedResponses as $pushedResponses) { foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) { - if ($pushDeferred instanceof DeferredFuture) { - $pushDeferred->error(new CancelledException()); - } else { - $pushDeferred->fail(new CancelledException()); - } + $pushDeferred->fail(new CancelledException()); $this->logger?->debug(\sprintf('Unused pushed response: "%s"', $pushedUrl)); } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a44989..508cf0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.0 +--- + + * Remove support for amphp/http-client < 5 + 7.4 --- diff --git a/Internal/AmpBodyV4.php b/Internal/AmpBodyV4.php deleted file mode 100644 index 78e2412..0000000 --- a/Internal/AmpBodyV4.php +++ /dev/null @@ -1,148 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Internal; - -use Amp\ByteStream\InputStream; -use Amp\ByteStream\ResourceInputStream; -use Amp\Http\Client\RequestBody; -use Amp\Promise; -use Amp\Success; -use Symfony\Component\HttpClient\Exception\TransportException; - -/** - * @author Nicolas Grekas - * - * @internal - */ -class AmpBodyV4 implements RequestBody, InputStream -{ - private ResourceInputStream|\Closure|string $body; - private array $info; - private ?int $offset = 0; - private int $length = -1; - private ?int $uploaded = null; - - /** - * @param \Closure|resource|string $body - */ - public function __construct( - $body, - &$info, - private \Closure $onProgress, - ) { - $this->info = &$info; - - if (\is_resource($body)) { - $this->offset = ftell($body); - $this->length = fstat($body)['size']; - $this->body = new ResourceInputStream($body); - } elseif (\is_string($body)) { - $this->length = \strlen($body); - $this->body = $body; - } else { - $this->body = $body; - } - } - - public function createBodyStream(): InputStream - { - if (null !== $this->uploaded) { - $this->uploaded = null; - - if (\is_string($this->body)) { - $this->offset = 0; - } elseif ($this->body instanceof ResourceInputStream) { - fseek($this->body->getResource(), $this->offset); - } - } - - return $this; - } - - public function getHeaders(): Promise - { - return new Success([]); - } - - public function getBodyLength(): Promise - { - return new Success($this->length - $this->offset); - } - - public function read(): Promise - { - $this->info['size_upload'] += $this->uploaded; - $this->uploaded = 0; - ($this->onProgress)(); - - $chunk = $this->doRead(); - $chunk->onResolve(function ($e, $data) { - if (null !== $data) { - $this->uploaded = \strlen($data); - } else { - $this->info['upload_content_length'] = $this->info['size_upload']; - } - }); - - return $chunk; - } - - public static function rewind(RequestBody $body): RequestBody - { - if (!$body instanceof self) { - return $body; - } - - $body->uploaded = null; - - if ($body->body instanceof ResourceInputStream) { - fseek($body->body->getResource(), $body->offset); - - return new $body($body->body, $body->info, $body->onProgress); - } - - if (\is_string($body->body)) { - $body->offset = 0; - } - - return $body; - } - - private function doRead(): Promise - { - if ($this->body instanceof ResourceInputStream) { - return $this->body->read(); - } - - if (null === $this->offset || !$this->length) { - return new Success(); - } - - if (\is_string($this->body)) { - $this->offset = null; - - return new Success($this->body); - } - - if ('' === $data = ($this->body)(16372)) { - $this->offset = null; - - return new Success(); - } - - if (!\is_string($data)) { - throw new TransportException(\sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data))); - } - - return new Success($data); - } -} diff --git a/Internal/AmpClientStateV4.php b/Internal/AmpClientStateV4.php deleted file mode 100644 index e02f4a0..0000000 --- a/Internal/AmpClientStateV4.php +++ /dev/null @@ -1,215 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Internal; - -use Amp\CancellationToken; -use Amp\Deferred; -use Amp\Http\Client\Connection\ConnectionLimitingPool; -use Amp\Http\Client\Connection\DefaultConnectionFactory; -use Amp\Http\Client\InterceptedHttpClient; -use Amp\Http\Client\Interceptor\RetryRequests; -use Amp\Http\Client\PooledHttpClient; -use Amp\Http\Client\Request; -use Amp\Http\Client\Response; -use Amp\Http\Tunnel\Http1TunnelConnector; -use Amp\Http\Tunnel\Https1TunnelConnector; -use Amp\Promise; -use Amp\Socket\Certificate; -use Amp\Socket\ClientTlsContext; -use Amp\Socket\ConnectContext; -use Amp\Socket\Connector; -use Amp\Socket\DnsConnector; -use Amp\Socket\SocketAddress; -use Amp\Success; -use Psr\Log\LoggerInterface; - -/** - * Internal representation of the Amp client's state. - * - * @author Nicolas Grekas - * - * @internal - */ -final class AmpClientStateV4 extends ClientState -{ - public array $dnsCache = []; - public int $responseCount = 0; - public array $pushedResponses = []; - - private array $clients = []; - private \Closure $clientConfigurator; - - public function __construct( - ?callable $clientConfigurator, - private int $maxHostConnections, - private int $maxPendingPushes, - private ?LoggerInterface &$logger, - ) { - $clientConfigurator ??= static fn (PooledHttpClient $client) => new InterceptedHttpClient($client, new RetryRequests(2)); - $this->clientConfigurator = $clientConfigurator(...); - } - - /** - * @return Promise - */ - public function request(array $options, Request $request, CancellationToken $cancellation, array &$info, \Closure $onProgress, &$handle): Promise - { - if ($options['proxy']) { - if ($request->hasHeader('proxy-authorization')) { - $options['proxy']['auth'] = $request->getHeader('proxy-authorization'); - } - - // Matching "no_proxy" should follow the behavior of curl - $host = $request->getUri()->getHost(); - foreach ($options['proxy']['no_proxy'] as $rule) { - $dotRule = '.'.ltrim($rule, '.'); - - if ('*' === $rule || $host === $rule || str_ends_with($host, $dotRule)) { - $options['proxy'] = null; - break; - } - } - } - - $request = clone $request; - - if ($request->hasHeader('proxy-authorization')) { - $request->removeHeader('proxy-authorization'); - } - - if ($options['capture_peer_cert_chain']) { - $info['peer_certificate_chain'] = []; - } - - $request->addEventListener(new AmpListenerV4($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle)); - $request->setPushHandler(fn ($request, $response): Promise => $this->handlePush($request, $response, $options)); - - ($request->hasHeader('content-length') ? new Success((int) $request->getHeader('content-length')) : $request->getBody()->getBodyLength()) - ->onResolve(static function ($e, $bodySize) use (&$info) { - if (null !== $bodySize && 0 <= $bodySize) { - $info['upload_content_length'] = ((1 + $info['upload_content_length']) ?? 1) - 1 + $bodySize; - } - }); - - [$client, $connector] = $this->getClient($options); - $response = $client->request($request, $cancellation); - $response->onResolve(static function ($e) use ($connector, &$handle) { - if (null === $e) { - $handle = $connector->handle; - } - }); - - return $response; - } - - private function getClient(array $options): array - { - $options = [ - 'bindto' => $options['bindto'] ?: '0', - 'verify_peer' => $options['verify_peer'], - 'capath' => $options['capath'], - 'cafile' => $options['cafile'], - 'local_cert' => $options['local_cert'], - 'local_pk' => $options['local_pk'], - 'ciphers' => $options['ciphers'], - 'capture_peer_cert_chain' => $options['capture_peer_cert_chain'] || $options['peer_fingerprint'], - 'proxy' => $options['proxy'], - 'crypto_method' => $options['crypto_method'], - ]; - - $key = hash('xxh128', serialize($options)); - - if (isset($this->clients[$key])) { - return $this->clients[$key]; - } - - $context = new ClientTlsContext(''); - $options['verify_peer'] || $context = $context->withoutPeerVerification(); - $options['cafile'] && $context = $context->withCaFile($options['cafile']); - $options['capath'] && $context = $context->withCaPath($options['capath']); - $options['local_cert'] && $context = $context->withCertificate(new Certificate($options['local_cert'], $options['local_pk'])); - $options['ciphers'] && $context = $context->withCiphers($options['ciphers']); - $options['capture_peer_cert_chain'] && $context = $context->withPeerCapturing(); - $options['crypto_method'] && $context = $context->withMinimumVersion($options['crypto_method']); - - $connector = $handleConnector = new class implements Connector { - public DnsConnector $connector; - public string $uri; - /** @var resource|null */ - public $handle; - - public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null): Promise - { - $result = $this->connector->connect($this->uri ?? $uri, $context, $token); - $result->onResolve(function ($e, $socket) { - $this->handle = $socket?->getResource(); - }); - - return $result; - } - }; - $connector->connector = new DnsConnector(new AmpResolverV4($this->dnsCache)); - - $context = (new ConnectContext()) - ->withTcpNoDelay() - ->withTlsContext($context); - - if ($options['bindto']) { - if (file_exists($options['bindto'])) { - $connector->uri = 'unix://'.$options['bindto']; - } else { - $context = $context->withBindTo($options['bindto']); - } - } - - if ($options['proxy']) { - $proxyUrl = parse_url($options['proxy']['url']); - $proxySocket = new SocketAddress($proxyUrl['host'], $proxyUrl['port']); - $proxyHeaders = $options['proxy']['auth'] ? ['Proxy-Authorization' => $options['proxy']['auth']] : []; - - if ('ssl' === $proxyUrl['scheme']) { - $connector = new Https1TunnelConnector($proxySocket, $context->getTlsContext(), $proxyHeaders, $connector); - } else { - $connector = new Http1TunnelConnector($proxySocket, $proxyHeaders, $connector); - } - } - - $maxHostConnections = 0 < $this->maxHostConnections ? $this->maxHostConnections : \PHP_INT_MAX; - $pool = new DefaultConnectionFactory($connector, $context); - $pool = ConnectionLimitingPool::byAuthority($maxHostConnections, $pool); - - return $this->clients[$key] = [($this->clientConfigurator)(new PooledHttpClient($pool)), $handleConnector]; - } - - private function handlePush(Request $request, Promise $response, array $options): Promise - { - $deferred = new Deferred(); - $authority = $request->getUri()->getAuthority(); - - if ($this->maxPendingPushes <= \count($this->pushedResponses[$authority] ?? [])) { - $fifoUrl = key($this->pushedResponses[$authority]); - unset($this->pushedResponses[$authority][$fifoUrl]); - $this->logger?->debug(\sprintf('Evicting oldest pushed response: "%s"', $fifoUrl)); - } - - $url = (string) $request->getUri(); - $this->logger?->debug(\sprintf('Queueing pushed response: "%s"', $url)); - $this->pushedResponses[$authority][] = [$url, $deferred, $request, $response, [ - 'proxy' => $options['proxy'], - 'bindto' => $options['bindto'], - 'local_cert' => $options['local_cert'], - 'local_pk' => $options['local_pk'], - ]]; - - return $deferred->promise(); - } -} diff --git a/Internal/AmpListenerV4.php b/Internal/AmpListenerV4.php deleted file mode 100644 index 9282fb4..0000000 --- a/Internal/AmpListenerV4.php +++ /dev/null @@ -1,184 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Internal; - -use Amp\Http\Client\Connection\Stream; -use Amp\Http\Client\EventListener; -use Amp\Http\Client\Request; -use Amp\Promise; -use Amp\Success; -use Symfony\Component\HttpClient\Exception\TransportException; - -/** - * @author Nicolas Grekas - * - * @internal - */ -class AmpListenerV4 implements EventListener -{ - private array $info; - - /** - * @param resource|null $handle - */ - public function __construct( - array &$info, - private array $pinSha256, - private \Closure $onProgress, - private &$handle, - ) { - $info += [ - 'connect_time' => 0.0, - 'pretransfer_time' => 0.0, - 'starttransfer_time' => 0.0, - 'total_time' => 0.0, - 'namelookup_time' => 0.0, - 'primary_ip' => '', - 'primary_port' => 0, - ]; - - $this->info = &$info; - } - - public function startRequest(Request $request): Promise - { - $this->info['start_time'] ??= microtime(true); - ($this->onProgress)(); - - return new Success(); - } - - public function startDnsResolution(Request $request): Promise - { - ($this->onProgress)(); - - return new Success(); - } - - public function startConnectionCreation(Request $request): Promise - { - ($this->onProgress)(); - - return new Success(); - } - - public function startTlsNegotiation(Request $request): Promise - { - ($this->onProgress)(); - - return new Success(); - } - - public function startSendingRequest(Request $request, Stream $stream): Promise - { - $host = $stream->getRemoteAddress()->getHost(); - $this->info['primary_ip'] = $host; - - if (str_contains($host, ':')) { - $host = '['.$host.']'; - } - - $this->info['primary_port'] = $stream->getRemoteAddress()->getPort(); - $this->info['pretransfer_time'] = microtime(true) - $this->info['start_time']; - $this->info['debug'] .= \sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']); - - if ((isset($this->info['peer_certificate_chain']) || $this->pinSha256) && null !== $tlsInfo = $stream->getTlsInfo()) { - foreach ($tlsInfo->getPeerCertificates() as $cert) { - $this->info['peer_certificate_chain'][] = openssl_x509_read($cert->toPem()); - } - - if ($this->pinSha256) { - $pin = openssl_pkey_get_public($this->info['peer_certificate_chain'][0]); - $pin = openssl_pkey_get_details($pin)['key']; - $pin = \array_slice(explode("\n", $pin), 1, -2); - $pin = base64_decode(implode('', $pin)); - $pin = base64_encode(hash('sha256', $pin, true)); - - if (!\in_array($pin, $this->pinSha256, true)) { - throw new TransportException(\sprintf('SSL public key does not match pinned public key for "%s".', $this->info['url'])); - } - } - } - ($this->onProgress)(); - - $uri = $request->getUri(); - $requestUri = $uri->getPath() ?: '/'; - - if ('' !== $query = $uri->getQuery()) { - $requestUri .= '?'.$query; - } - - if ('CONNECT' === $method = $request->getMethod()) { - $requestUri = $uri->getHost().': '.($uri->getPort() ?? ('https' === $uri->getScheme() ? 443 : 80)); - } - - $this->info['debug'] .= \sprintf("> %s %s HTTP/%s \r\n", $method, $requestUri, $request->getProtocolVersions()[0]); - - foreach ($request->getRawHeaders() as [$name, $value]) { - $this->info['debug'] .= $name.': '.$value."\r\n"; - } - $this->info['debug'] .= "\r\n"; - - return new Success(); - } - - public function completeSendingRequest(Request $request, Stream $stream): Promise - { - ($this->onProgress)(); - - return new Success(); - } - - public function startReceivingResponse(Request $request, Stream $stream): Promise - { - $this->info['starttransfer_time'] = microtime(true) - $this->info['start_time']; - ($this->onProgress)(); - - return new Success(); - } - - public function completeReceivingResponse(Request $request, Stream $stream): Promise - { - $this->handle = null; - ($this->onProgress)(); - - return new Success(); - } - - public function completeDnsResolution(Request $request): Promise - { - $this->info['namelookup_time'] = microtime(true) - $this->info['start_time']; - ($this->onProgress)(); - - return new Success(); - } - - public function completeConnectionCreation(Request $request): Promise - { - $this->info['connect_time'] = microtime(true) - $this->info['start_time']; - ($this->onProgress)(); - - return new Success(); - } - - public function completeTlsNegotiation(Request $request): Promise - { - ($this->onProgress)(); - - return new Success(); - } - - public function abort(Request $request, \Throwable $cause): Promise - { - return new Success(); - } -} diff --git a/Internal/AmpResolverV4.php b/Internal/AmpResolverV4.php deleted file mode 100644 index ffc45c8..0000000 --- a/Internal/AmpResolverV4.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Internal; - -use Amp\Dns; -use Amp\Dns\Record; -use Amp\Promise; -use Amp\Success; - -/** - * Handles local overrides for the DNS resolver. - * - * @author Nicolas Grekas - * - * @internal - */ -class AmpResolverV4 implements Dns\Resolver -{ - public function __construct( - private array &$dnsMap, - ) { - } - - public function resolve(string $name, ?int $typeRestriction = null): Promise - { - $recordType = Record::A; - $ip = $this->dnsMap[$name] ?? null; - - if (null !== $ip && str_contains($ip, ':')) { - $recordType = Record::AAAA; - } - if (null === $ip || $recordType !== ($typeRestriction ?? $recordType)) { - return Dns\resolver()->resolve($name, $typeRestriction); - } - - return new Success([new Record($ip, $recordType, null)]); - } - - public function query(string $name, int $type): Promise - { - $recordType = Record::A; - $ip = $this->dnsMap[$name] ?? null; - - if (null !== $ip && str_contains($ip, ':')) { - $recordType = Record::AAAA; - } - if (null === $ip || $recordType !== $type) { - return Dns\resolver()->query($name, $type); - } - - return new Success([new Record($ip, $recordType, null)]); - } -} diff --git a/Response/AmpResponseV4.php b/Response/AmpResponseV4.php deleted file mode 100644 index 31ea22d..0000000 --- a/Response/AmpResponseV4.php +++ /dev/null @@ -1,458 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Response; - -use Amp\ByteStream\StreamException; -use Amp\CancellationTokenSource; -use Amp\Coroutine; -use Amp\Deferred; -use Amp\Http\Client\HttpException; -use Amp\Http\Client\Request; -use Amp\Http\Client\Response; -use Amp\Loop; -use Amp\Promise; -use Amp\Success; -use Psr\Log\LoggerInterface; -use Symfony\Component\HttpClient\Chunk\FirstChunk; -use Symfony\Component\HttpClient\Chunk\InformationalChunk; -use Symfony\Component\HttpClient\Exception\InvalidArgumentException; -use Symfony\Component\HttpClient\Exception\TransportException; -use Symfony\Component\HttpClient\HttpClientTrait; -use Symfony\Component\HttpClient\Internal\AmpBodyV4; -use Symfony\Component\HttpClient\Internal\AmpClientStateV4; -use Symfony\Component\HttpClient\Internal\Canary; -use Symfony\Component\HttpClient\Internal\ClientState; -use Symfony\Contracts\HttpClient\ResponseInterface; - -/** - * @author Nicolas Grekas - * - * @internal - */ -final class AmpResponseV4 implements ResponseInterface, StreamableInterface -{ - use CommonResponseTrait; - use TransportResponseTrait; - - private static string $nextId = 'a'; - - private ?array $options; - private \Closure $onProgress; - - private static ?string $delay = null; - - /** - * @internal - */ - public function __construct( - private AmpClientStateV4 $multi, - Request $request, - array $options, - ?LoggerInterface $logger, - ) { - $this->options = &$options; - $this->logger = $logger; - $this->timeout = $options['timeout']; - $this->shouldBuffer = $options['buffer']; - - if ($this->inflate = \extension_loaded('zlib') && !$request->hasHeader('accept-encoding')) { - $request->setHeader('Accept-Encoding', 'gzip'); - } - - $this->initializer = static fn (self $response) => null !== $response->options; - - $info = &$this->info; - $headers = &$this->headers; - $canceller = new CancellationTokenSource(); - $handle = &$this->handle; - - $info['url'] = (string) $request->getUri(); - $info['http_method'] = $request->getMethod(); - $info['start_time'] = null; - $info['redirect_url'] = null; - $info['original_url'] = $info['url']; - $info['redirect_time'] = 0.0; - $info['redirect_count'] = 0; - $info['size_upload'] = 0.0; - $info['size_download'] = 0.0; - $info['upload_content_length'] = -1.0; - $info['download_content_length'] = -1.0; - $info['user_data'] = $options['user_data']; - $info['max_duration'] = $options['max_duration']; - $info['debug'] = ''; - - $onProgress = $options['on_progress'] ?? static function () {}; - $onProgress = $this->onProgress = static function () use (&$info, $onProgress) { - $info['total_time'] = microtime(true) - $info['start_time']; - $onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info); - }; - - $pauseDeferred = new Deferred(); - $pause = new Success(); - - $throttleWatcher = null; - - $this->id = $id = self::$nextId++; - Loop::defer(static function () use ($request, $multi, $id, &$info, &$headers, $canceller, &$options, $onProgress, &$handle, $logger, &$pause) { - return new Coroutine(self::generateResponse($request, $multi, $id, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause)); - }); - - $info['pause_handler'] = static function (float $duration) use (&$throttleWatcher, &$pauseDeferred, &$pause) { - if (null !== $throttleWatcher) { - Loop::cancel($throttleWatcher); - } - - $pause = $pauseDeferred->promise(); - - if ($duration <= 0) { - $deferred = $pauseDeferred; - $pauseDeferred = new Deferred(); - $deferred->resolve(); - } else { - $throttleWatcher = Loop::delay(ceil(1000 * $duration), static function () use (&$pauseDeferred) { - $deferred = $pauseDeferred; - $pauseDeferred = new Deferred(); - $deferred->resolve(); - }); - } - }; - - $multi->lastTimeout = null; - $multi->openHandles[$id] = $id; - ++$multi->responseCount; - - $this->canary = new Canary(static function () use ($canceller, $multi, $id) { - $canceller->cancel(); - unset($multi->openHandles[$id], $multi->handlesActivity[$id]); - }); - } - - public function getInfo(?string $type = null): mixed - { - return null !== $type ? $this->info[$type] ?? null : $this->info; - } - - public function __sleep(): array - { - throw new \BadMethodCallException('Cannot serialize '.__CLASS__); - } - - public function __wakeup(): void - { - throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); - } - - public function __destruct() - { - try { - $this->doDestruct(); - } finally { - // Clear the DNS cache when all requests completed - if (0 >= --$this->multi->responseCount) { - $this->multi->responseCount = 0; - $this->multi->dnsCache = []; - } - } - } - - private static function schedule(self $response, array &$runningResponses): void - { - if (isset($runningResponses[0])) { - $runningResponses[0][1][$response->id] = $response; - } else { - $runningResponses[0] = [$response->multi, [$response->id => $response]]; - } - - if (!isset($response->multi->openHandles[$response->id])) { - $response->multi->handlesActivity[$response->id][] = null; - $response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null; - } - } - - /** - * @param AmpClientStateV4 $multi - */ - private static function perform(ClientState $multi, ?array $responses = null): void - { - foreach ($responses ?? [] as $response) { - try { - if ($response->info['start_time']) { - $response->info['total_time'] = microtime(true) - $response->info['start_time']; - ($response->onProgress)(); - } - } catch (\Throwable $e) { - $multi->handlesActivity[$response->id][] = null; - $multi->handlesActivity[$response->id][] = $e; - } - } - } - - /** - * @param AmpClientStateV4 $multi - */ - private static function select(ClientState $multi, float $timeout): int - { - $timeout += hrtime(true) / 1E9; - self::$delay = Loop::defer(static function () use ($timeout) { - if (0 < $timeout -= hrtime(true) / 1E9) { - self::$delay = Loop::delay(ceil(1000 * $timeout), Loop::stop(...)); - } else { - Loop::stop(); - } - }); - - Loop::run(); - - return null === self::$delay ? 1 : 0; - } - - private static function generateResponse(Request $request, AmpClientStateV4 $multi, string $id, array &$info, array &$headers, CancellationTokenSource $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause): \Generator - { - $request->setInformationalResponseHandler(static function (Response $response) use ($multi, $id, &$info, &$headers) { - self::addResponseHeaders($response, $info, $headers); - $multi->handlesActivity[$id][] = new InformationalChunk($response->getStatus(), $response->getHeaders()); - self::stopLoop(); - }); - - try { - /* @var Response $response */ - if (null === $response = yield from self::getPushedResponse($request, $multi, $info, $headers, $options, $logger)) { - $logger?->info(\sprintf('Request: "%s %s"', $info['http_method'], $info['url'])); - - $response = yield from self::followRedirects($request, $multi, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause); - } - - $options = null; - - $multi->handlesActivity[$id][] = new FirstChunk(); - - if ('HEAD' === $response->getRequest()->getMethod() || \in_array($info['http_code'], [204, 304], true)) { - $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = null; - self::stopLoop(); - - return; - } - - if ($response->hasHeader('content-length')) { - $info['download_content_length'] = (float) $response->getHeader('content-length'); - } - - $body = $response->getBody(); - - while (true) { - self::stopLoop(); - - yield $pause; - - if (null === $data = yield $body->read()) { - break; - } - - $info['size_download'] += \strlen($data); - $multi->handlesActivity[$id][] = $data; - } - - $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = null; - } catch (\Throwable $e) { - $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = $e; - } finally { - $info['download_content_length'] = $info['size_download']; - } - - self::stopLoop(); - } - - private static function followRedirects(Request $originRequest, AmpClientStateV4 $multi, array &$info, array &$headers, CancellationTokenSource $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause): \Generator - { - yield $pause; - - $originRequest->setBody(new AmpBodyV4($options['body'], $info, $onProgress)); - $response = yield $multi->request($options, $originRequest, $canceller->getToken(), $info, $onProgress, $handle); - $previousUrl = null; - - while (true) { - self::addResponseHeaders($response, $info, $headers); - $status = $response->getStatus(); - - if (!\in_array($status, [301, 302, 303, 307, 308], true) || null === $location = $response->getHeader('location')) { - return $response; - } - - $urlResolver = new class { - use HttpClientTrait { - parseUrl as public; - resolveUrl as public; - } - }; - - try { - $previousUrl ??= $urlResolver::parseUrl($info['url']); - $location = $urlResolver::parseUrl($location); - $location = $urlResolver::resolveUrl($location, $previousUrl); - $info['redirect_url'] = implode('', $location); - } catch (InvalidArgumentException) { - return $response; - } - - if (0 >= $options['max_redirects'] || $info['redirect_count'] >= $options['max_redirects']) { - return $response; - } - - $logger?->info(\sprintf('Redirecting: "%s %s"', $status, $info['url'])); - - try { - // Discard body of redirects - while (null !== yield $response->getBody()->read()) { - } - } catch (HttpException|StreamException) { - // Ignore streaming errors on previous responses - } - - ++$info['redirect_count']; - $info['url'] = $info['redirect_url']; - $info['redirect_url'] = null; - $previousUrl = $location; - - $request = new Request($info['url'], $info['http_method']); - $request->setProtocolVersions($originRequest->getProtocolVersions()); - $request->setTcpConnectTimeout($originRequest->getTcpConnectTimeout()); - $request->setTlsHandshakeTimeout($originRequest->getTlsHandshakeTimeout()); - $request->setTransferTimeout($originRequest->getTransferTimeout()); - - if (303 === $status || \in_array($status, [301, 302], true) && 'POST' === $response->getRequest()->getMethod()) { - // Do like curl and browsers: turn POST to GET on 301, 302 and 303 - $originRequest->removeHeader('transfer-encoding'); - $originRequest->removeHeader('content-length'); - $originRequest->removeHeader('content-type'); - - $info['http_method'] = 'HEAD' === $response->getRequest()->getMethod() ? 'HEAD' : 'GET'; - $request->setMethod($info['http_method']); - } else { - $request->setBody(AmpBodyV4::rewind($response->getRequest()->getBody())); - } - - foreach ($originRequest->getRawHeaders() as [$name, $value]) { - $request->addHeader($name, $value); - } - - if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) { - $request->removeHeader('authorization'); - $request->removeHeader('cookie'); - $request->removeHeader('host'); - } - - yield $pause; - - $response = yield $multi->request($options, $request, $canceller->getToken(), $info, $onProgress, $handle); - $info['redirect_time'] = microtime(true) - $info['start_time']; - } - } - - private static function addResponseHeaders(Response $response, array &$info, array &$headers): void - { - $info['http_code'] = $response->getStatus(); - - if ($headers) { - $info['debug'] .= "< \r\n"; - $headers = []; - } - - $h = \sprintf('HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatus(), $response->getReason()); - $info['debug'] .= "< {$h}\r\n"; - $info['response_headers'][] = $h; - - foreach ($response->getRawHeaders() as [$name, $value]) { - $headers[strtolower($name)][] = $value; - $h = $name.': '.$value; - $info['debug'] .= "< {$h}\r\n"; - $info['response_headers'][] = $h; - } - - $info['debug'] .= "< \r\n"; - } - - /** - * Accepts pushed responses only if their headers related to authentication match the request. - */ - private static function getPushedResponse(Request $request, AmpClientStateV4 $multi, array &$info, array &$headers, array $options, ?LoggerInterface $logger): \Generator - { - if ('' !== $options['body']) { - return null; - } - - $authority = $request->getUri()->getAuthority(); - - foreach ($multi->pushedResponses[$authority] ?? [] as $i => [$pushedUrl, $pushDeferred, $pushedRequest, $pushedResponse, $parentOptions]) { - if ($info['url'] !== $pushedUrl || $info['http_method'] !== $pushedRequest->getMethod()) { - continue; - } - - foreach ($parentOptions as $k => $v) { - if ($options[$k] !== $v) { - continue 2; - } - } - - foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) { - if ($pushedRequest->getHeaderArray($k) !== $request->getHeaderArray($k)) { - continue 2; - } - } - - $response = yield $pushedResponse; - - foreach ($response->getHeaderArray('vary') as $vary) { - foreach (preg_split('/\s*+,\s*+/', $vary) as $v) { - if ('*' === $v || ($pushedRequest->getHeaderArray($v) !== $request->getHeaderArray($v) && 'accept-encoding' !== strtolower($v))) { - $logger?->debug(\sprintf('Skipping pushed response: "%s"', $info['url'])); - continue 3; - } - } - } - - $info += [ - 'connect_time' => 0.0, - 'pretransfer_time' => 0.0, - 'starttransfer_time' => 0.0, - 'total_time' => 0.0, - 'namelookup_time' => 0.0, - 'primary_ip' => '', - 'primary_port' => 0, - 'start_time' => microtime(true), - ]; - - $pushDeferred->resolve(); - $logger?->debug(\sprintf('Accepting pushed response: "%s %s"', $info['http_method'], $info['url'])); - self::addResponseHeaders($response, $info, $headers); - unset($multi->pushedResponses[$authority][$i]); - - if (!$multi->pushedResponses[$authority]) { - unset($multi->pushedResponses[$authority]); - } - - return $response; - } - } - - private static function stopLoop(): void - { - if (null !== self::$delay) { - Loop::cancel(self::$delay); - self::$delay = null; - } - - Loop::defer(Loop::stop(...)); - } -} diff --git a/composer.json b/composer.json index 3b5ff49..aececcb 100644 --- a/composer.json +++ b/composer.json @@ -29,13 +29,12 @@ "symfony/service-contracts": "^2.5|^3" }, "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", + "amphp/http-client": "^5.0", + "amphp/http-tunnel": "^2.0", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", "symfony/dependency-injection": "^7.4|^8.0", "symfony/http-kernel": "^7.4|^8.0", "symfony/messenger": "^7.4|^8.0", @@ -44,8 +43,7 @@ "symfony/stopwatch": "^7.4|^8.0" }, "conflict": { - "amphp/amp": "<2.5", - "amphp/socket": "<1.1", + "amphp/amp": "<3", "php-http/discovery": "<1.15" }, "autoload": { From 7720548d1efd96ff160ba88d7090e82813e34904 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Jun 2025 17:03:41 +0200 Subject: [PATCH 7/9] [HttpClient] Rename amphp "V5" classes --- AmpHttpClient.php | 14 ++++++------ Internal/{AmpBodyV5.php => AmpBody.php} | 2 +- ...mpClientStateV5.php => AmpClientState.php} | 6 ++--- .../{AmpListenerV5.php => AmpListener.php} | 2 +- .../{AmpResolverV5.php => AmpResolver.php} | 2 +- .../{AmpResponseV5.php => AmpResponse.php} | 22 +++++++++---------- 6 files changed, 24 insertions(+), 24 deletions(-) rename Internal/{AmpBodyV5.php => AmpBody.php} (98%) rename Internal/{AmpClientStateV5.php => AmpClientState.php} (97%) rename Internal/{AmpListenerV5.php => AmpListener.php} (99%) rename Internal/{AmpResolverV5.php => AmpResolver.php} (97%) rename Response/{AmpResponseV5.php => AmpResponse.php} (94%) diff --git a/AmpHttpClient.php b/AmpHttpClient.php index cc858df..7df0ee6 100644 --- a/AmpHttpClient.php +++ b/AmpHttpClient.php @@ -20,8 +20,8 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\HttpClient\Exception\TransportException; -use Symfony\Component\HttpClient\Internal\AmpClientStateV5; -use Symfony\Component\HttpClient\Response\AmpResponseV5; +use Symfony\Component\HttpClient\Internal\AmpClientState; +use Symfony\Component\HttpClient\Response\AmpResponse; use Symfony\Component\HttpClient\Response\ResponseStream; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -48,7 +48,7 @@ final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, private array $defaultOptions = self::OPTIONS_DEFAULTS; private static array $emptyDefaults = self::OPTIONS_DEFAULTS; - private AmpClientStateV5 $multi; + private AmpClientState $multi; /** * @param array $defaultOptions Default requests' options @@ -67,7 +67,7 @@ public function __construct(array $defaultOptions = [], ?callable $clientConfigu [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); } - $this->multi = new AmpClientStateV5($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger); + $this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger); } /** @@ -141,16 +141,16 @@ public function request(string $method, string $url, array $options = []): Respo $request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth))); } - return new AmpResponseV5($this->multi, $request, $options, $this->logger); + return new AmpResponse($this->multi, $request, $options, $this->logger); } public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { - if ($responses instanceof AmpResponseV5) { + if ($responses instanceof AmpResponse) { $responses = [$responses]; } - return new ResponseStream(AmpResponseV5::stream($responses, $timeout)); + return new ResponseStream(AmpResponse::stream($responses, $timeout)); } public function reset(): void diff --git a/Internal/AmpBodyV5.php b/Internal/AmpBody.php similarity index 98% rename from Internal/AmpBodyV5.php rename to Internal/AmpBody.php index 70e8a61..a31f77c 100644 --- a/Internal/AmpBodyV5.php +++ b/Internal/AmpBody.php @@ -24,7 +24,7 @@ * * @internal */ -class AmpBodyV5 implements HttpContent, ReadableStream, \IteratorAggregate +class AmpBody implements HttpContent, ReadableStream, \IteratorAggregate { private ReadableStream $body; private ?string $content; diff --git a/Internal/AmpClientStateV5.php b/Internal/AmpClientState.php similarity index 97% rename from Internal/AmpClientStateV5.php rename to Internal/AmpClientState.php index f1ee284..e468e55 100644 --- a/Internal/AmpClientStateV5.php +++ b/Internal/AmpClientState.php @@ -41,7 +41,7 @@ * * @internal */ -final class AmpClientStateV5 extends ClientState +final class AmpClientState extends ClientState { public array $dnsCache = []; public int $responseCount = 0; @@ -87,7 +87,7 @@ public function request(array $options, Request $request, Cancellation $cancella $info['peer_certificate_chain'] = []; } - $request->addEventListener(new AmpListenerV5($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle)); + $request->addEventListener(new AmpListener($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle)); $request->setPushHandler(fn ($request, $response) => $this->handlePush($request, $response, $options)); if (0 <= $bodySize = $request->hasHeader('content-length') ? (int) $request->getHeader('content-length') : $request->getBody()->getContentLength() ?? -1) { @@ -145,7 +145,7 @@ public function connect(SocketAddress|string $uri, ?ConnectContext $context = nu return $socket; } }; - $connector->connector = new DnsSocketConnector(new AmpResolverV5($this->dnsCache)); + $connector->connector = new DnsSocketConnector(new AmpResolver($this->dnsCache)); $context = (new ConnectContext()) ->withTcpNoDelay() diff --git a/Internal/AmpListenerV5.php b/Internal/AmpListener.php similarity index 99% rename from Internal/AmpListenerV5.php rename to Internal/AmpListener.php index 92dcba8..66dc6df 100644 --- a/Internal/AmpListenerV5.php +++ b/Internal/AmpListener.php @@ -26,7 +26,7 @@ * * @internal */ -class AmpListenerV5 implements EventListener +class AmpListener implements EventListener { private array $info; diff --git a/Internal/AmpResolverV5.php b/Internal/AmpResolver.php similarity index 97% rename from Internal/AmpResolverV5.php rename to Internal/AmpResolver.php index 4ef56ec..2fc1485 100644 --- a/Internal/AmpResolverV5.php +++ b/Internal/AmpResolver.php @@ -23,7 +23,7 @@ * * @internal */ -class AmpResolverV5 implements DnsResolver +class AmpResolver implements DnsResolver { public function __construct( private array &$dnsMap, diff --git a/Response/AmpResponseV5.php b/Response/AmpResponse.php similarity index 94% rename from Response/AmpResponseV5.php rename to Response/AmpResponse.php index 8f56c76..2d9b180 100644 --- a/Response/AmpResponseV5.php +++ b/Response/AmpResponse.php @@ -25,8 +25,8 @@ use Symfony\Component\HttpClient\Exception\InvalidArgumentException; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\HttpClientTrait; -use Symfony\Component\HttpClient\Internal\AmpBodyV5; -use Symfony\Component\HttpClient\Internal\AmpClientStateV5; +use Symfony\Component\HttpClient\Internal\AmpBody; +use Symfony\Component\HttpClient\Internal\AmpClientState; use Symfony\Component\HttpClient\Internal\Canary; use Symfony\Component\HttpClient\Internal\ClientState; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -39,7 +39,7 @@ * * @internal */ -final class AmpResponseV5 implements ResponseInterface, StreamableInterface +final class AmpResponse implements ResponseInterface, StreamableInterface { use CommonResponseTrait; use TransportResponseTrait; @@ -53,7 +53,7 @@ final class AmpResponseV5 implements ResponseInterface, StreamableInterface * @internal */ public function __construct( - private AmpClientStateV5 $multi, + private AmpClientState $multi, Request $request, array $options, ?LoggerInterface $logger, @@ -160,7 +160,7 @@ private static function schedule(self $response, array &$runningResponses): void } /** - * @param AmpClientStateV5 $multi + * @param AmpClientState $multi */ private static function perform(ClientState $multi, ?array $responses = null): void { @@ -180,7 +180,7 @@ private static function perform(ClientState $multi, ?array $responses = null): v } /** - * @param AmpClientStateV5 $multi + * @param AmpClientState $multi */ private static function select(ClientState $multi, float $timeout): int { @@ -205,7 +205,7 @@ private static function select(ClientState $multi, float $timeout): int return 1; } - private static function generateResponse(Request $request, AmpClientStateV5 $multi, string $id, array &$info, array &$headers, DeferredCancellation $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, float &$pause): void + private static function generateResponse(Request $request, AmpClientState $multi, string $id, array &$info, array &$headers, DeferredCancellation $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, float &$pause): void { $request->setInformationalResponseHandler(static function (Response $response) use ($multi, $id, &$info, &$headers) { self::addResponseHeaders($response, $info, $headers); @@ -265,13 +265,13 @@ private static function generateResponse(Request $request, AmpClientStateV5 $mul } } - private static function followRedirects(Request $originRequest, AmpClientStateV5 $multi, array &$info, array &$headers, DeferredCancellation $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, float &$pause): ?Response + private static function followRedirects(Request $originRequest, AmpClientState $multi, array &$info, array &$headers, DeferredCancellation $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, float &$pause): ?Response { if (0 < $pause) { delay($pause, true, $canceller->getCancellation()); } - $originRequest->setBody(new AmpBodyV5($options['body'], $info, $onProgress)); + $originRequest->setBody(new AmpBody($options['body'], $info, $onProgress)); $response = $multi->request($options, $originRequest, $canceller->getCancellation(), $info, $onProgress, $handle); $previousUrl = null; @@ -334,7 +334,7 @@ private static function followRedirects(Request $originRequest, AmpClientStateV5 $request->setMethod($info['http_method']); } } else { - $request->setBody(AmpBodyV5::rewind($response->getRequest()->getBody())); + $request->setBody(AmpBody::rewind($response->getRequest()->getBody())); } foreach ($originRequest->getHeaderPairs() as [$name, $value]) { @@ -382,7 +382,7 @@ private static function addResponseHeaders(Response $response, array &$info, arr /** * Accepts pushed responses only if their headers related to authentication match the request. */ - private static function getPushedResponse(Request $request, AmpClientStateV5 $multi, array &$info, array &$headers, DeferredCancellation $canceller, array $options, ?LoggerInterface $logger): ?Response + private static function getPushedResponse(Request $request, AmpClientState $multi, array &$info, array &$headers, DeferredCancellation $canceller, array $options, ?LoggerInterface $logger): ?Response { if ('' !== $options['body']) { return null; From 4402ec32582d3a1cb3d6132058cb602e82779108 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Jun 2025 17:07:35 +0200 Subject: [PATCH 8/9] [HttpClient] Remove setLogger() methods on decorators --- CHANGELOG.md | 1 + NoPrivateNetworkHttpClient.php | 15 +-------------- ScopingHttpClient.php | 15 +-------------- TraceableHttpClient.php | 15 +-------------- composer.json | 1 - 5 files changed, 4 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 508cf0e..98dc3e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Remove support for amphp/http-client < 5 + * Remove setLogger() methods on decorators; configure the logger on the wrapped client directly instead 7.4 --- diff --git a/NoPrivateNetworkHttpClient.php b/NoPrivateNetworkHttpClient.php index ff63e56..0fd63c8 100644 --- a/NoPrivateNetworkHttpClient.php +++ b/NoPrivateNetworkHttpClient.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpClient; -use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\Response\AsyncContext; @@ -28,7 +27,7 @@ * @author Hallison Boaventura * @author Nicolas Grekas */ -final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface +final class NoPrivateNetworkHttpClient implements HttpClientInterface, ResetInterface { use AsyncDecoratorTrait; use HttpClientTrait; @@ -159,18 +158,6 @@ public function request(string $method, string $url, array $options = []): Respo }); } - /** - * @deprecated since Symfony 7.1, configure the logger on the wrapped HTTP client directly instead - */ - public function setLogger(LoggerInterface $logger): void - { - trigger_deprecation('symfony/http-client', '7.1', 'Configure the logger on the wrapped HTTP client directly instead.'); - - if ($this->client instanceof LoggerAwareInterface) { - $this->client->setLogger($logger); - } - } - public function withOptions(array $options): static { $clone = clone $this; diff --git a/ScopingHttpClient.php b/ScopingHttpClient.php index 185cf21..0edcb4f 100644 --- a/ScopingHttpClient.php +++ b/ScopingHttpClient.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpClient; -use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\Exception\InvalidArgumentException; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -24,7 +23,7 @@ * * @author Anthony Martin */ -class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface +class ScopingHttpClient implements HttpClientInterface, ResetInterface { use HttpClientTrait; @@ -95,18 +94,6 @@ public function reset(): void } } - /** - * @deprecated since Symfony 7.1, configure the logger on the wrapped HTTP client directly instead - */ - public function setLogger(LoggerInterface $logger): void - { - trigger_deprecation('symfony/http-client', '7.1', 'Configure the logger on the wrapped HTTP client directly instead.'); - - if ($this->client instanceof LoggerAwareInterface) { - $this->client->setLogger($logger); - } - } - public function withOptions(array $options): static { $clone = clone $this; diff --git a/TraceableHttpClient.php b/TraceableHttpClient.php index e65a5cc..6c10f92 100644 --- a/TraceableHttpClient.php +++ b/TraceableHttpClient.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpClient; -use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\Response\ResponseStream; use Symfony\Component\HttpClient\Response\TraceableResponse; @@ -24,7 +23,7 @@ /** * @author Jérémy Romey */ -final class TraceableHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface +final class TraceableHttpClient implements HttpClientInterface, ResetInterface { private \ArrayObject $tracedRequests; @@ -94,18 +93,6 @@ public function reset(): void $this->tracedRequests->exchangeArray([]); } - /** - * @deprecated since Symfony 7.1, configure the logger on the wrapped HTTP client directly instead - */ - public function setLogger(LoggerInterface $logger): void - { - trigger_deprecation('symfony/http-client', '7.1', 'Configure the logger on the wrapped HTTP client directly instead.'); - - if ($this->client instanceof LoggerAwareInterface) { - $this->client->setLogger($logger); - } - } - public function withOptions(array $options): static { $clone = clone $this; diff --git a/composer.json b/composer.json index aececcb..3d682a2 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "require": { "php": ">=8.4", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, From 9ab359e61113f4c46cdd84796fed125d22cad13f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 6 Jun 2025 17:06:38 +0200 Subject: [PATCH 9/9] [HttpClient] Fix low-deps job --- AmpHttpClient.php | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AmpHttpClient.php b/AmpHttpClient.php index 7df0ee6..13c5320 100644 --- a/AmpHttpClient.php +++ b/AmpHttpClient.php @@ -159,7 +159,7 @@ public function reset(): void foreach ($this->multi->pushedResponses as $pushedResponses) { foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) { - $pushDeferred->fail(new CancelledException()); + $pushDeferred->error(new CancelledException()); $this->logger?->debug(\sprintf('Unused pushed response: "%s"', $pushedUrl)); } diff --git a/composer.json b/composer.json index 3d682a2..16a3219 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "symfony/service-contracts": "^2.5|^3" }, "require-dev": { - "amphp/http-client": "^5.0", + "amphp/http-client": "^5.3.2", "amphp/http-tunnel": "^2.0", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0",