Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[HttpClient] Add support for amphp/http-client v5 #54179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/patch-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/'):
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberIntersectionWithTrait.php'):
case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'):
case false !== strpos($file, '/src/Symfony/Component/HttpClient/Internal/'):
case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Answer.php'):
case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Number.php'):
case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Suit.php'):
Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,8 @@
"symfony/yaml": "self.version"
},
"require-dev": {
"amphp/amp": "^2.5",
"amphp/http-client": "^4.2.1",
"amphp/http-tunnel": "^1.0",
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"async-aws/ses": "^1.0",
"async-aws/sqs": "^1.0|^2.0",
"async-aws/sns": "^1.0",
Expand All @@ -151,6 +150,7 @@
"psr/http-client": "^1.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
"seld/jsonlint": "^1.10",
"symfony/amphp-http-client-meta": "^1.0|^2.0",
"symfony/mercure-bundle": "^0.3",
"symfony/phpunit-bridge": "^6.4|^7.0",
"symfony/runtime": "self.version",
Expand All @@ -162,6 +162,7 @@
},
"conflict": {
"ext-psr": "<1.1|>=2",
"amphp/amp": "<2.5",
"async-aws/core": "<1.5",
"doctrine/collections": "<1.8",
"doctrine/dbal": "<3.6",
Expand Down
50 changes: 35 additions & 15 deletions src/Symfony/Component/HttpClient/AmpHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
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 Amp\Promise;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\AmpClientState;
use Symfony\Component\HttpClient\Response\AmpResponse;
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;
use Symfony\Contracts\HttpClient\ResponseInterface;
Expand All @@ -33,8 +36,8 @@
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 (!interface_exists(Promise::class)) {
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the installed "amphp/http-client" is not compatible with this version of "symfony/http-client". Try downgrading "amphp/http-client" to "^4.2.1".');
if (\PHP_VERSION_ID < 80400 && is_subclass_of(Request::class, HttpMessage::class)) {

Check failure on line 39 in src/Symfony/Component/HttpClient/AmpHttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/HttpClient/AmpHttpClient.php:39:63: UndefinedClass: Class, interface or enum named Amp\Http\HttpMessage does not exist (see https://psalm.dev/019)
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.');
}

/**
Expand All @@ -53,7 +56,7 @@

private array $defaultOptions = self::OPTIONS_DEFAULTS;
private static array $emptyDefaults = self::OPTIONS_DEFAULTS;
private AmpClientState $multi;
private AmpClientStateV4|AmpClientStateV5 $multi;

/**
* @param array $defaultOptions Default requests' options
Expand All @@ -72,7 +75,11 @@
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}

$this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
if (is_subclass_of(Request::class, HttpMessage::class)) {

Check failure on line 78 in src/Symfony/Component/HttpClient/AmpHttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/HttpClient/AmpHttpClient.php:78:44: UndefinedClass: Class, interface or enum named Amp\Http\HttpMessage does not exist (see https://psalm.dev/019)
$this->multi = new AmpClientStateV5($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
} else {
$this->multi = new AmpClientStateV4($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
}
}

/**
Expand Down Expand Up @@ -132,9 +139,10 @@
$request->addHeader($h[0], $h[1]);
}

$request->setTcpConnectTimeout(1000 * $options['timeout']);
$request->setTlsHandshakeTimeout(1000 * $options['timeout']);
$request->setTransferTimeout(1000 * $options['max_duration']);
$coef = $request instanceof HttpMessage ? 1 : 1000;

Check failure on line 142 in src/Symfony/Component/HttpClient/AmpHttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/HttpClient/AmpHttpClient.php:142:37: UndefinedClass: Class, interface or enum named Amp\Http\HttpMessage does not exist (see https://psalm.dev/019)
$request->setTcpConnectTimeout($coef * $options['timeout']);
$request->setTlsHandshakeTimeout($coef * $options['timeout']);
$request->setTransferTimeout($coef * $options['max_duration']);
if (method_exists($request, 'setInactivityTimeout')) {
$request->setInactivityTimeout(0);
}
Expand All @@ -145,25 +153,37 @@
$request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth)));
}

return new AmpResponse($this->multi, $request, $options, $this->logger);
if ($request instanceof HttpMessage) {
return new AmpResponseV5($this->multi, $request, $options, $this->logger);
}

return new AmpResponseV4($this->multi, $request, $options, $this->logger);
}

public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof AmpResponse) {
if ($responses instanceof AmpResponseV4 || $responses instanceof AmpResponseV5) {
$responses = [$responses];
}

return new ResponseStream(AmpResponse::stream($responses, $timeout));
if ($this->multi instanceof AmpClientStateV5) {
return new ResponseStream(AmpResponseV5::stream($responses, $timeout));

Check failure on line 170 in src/Symfony/Component/HttpClient/AmpHttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidArgument

src/Symfony/Component/HttpClient/AmpHttpClient.php:170:61: InvalidArgument: Argument 1 of Symfony\Component\HttpClient\Response\AmpResponseV5::stream expects iterable<array-key, Symfony\Component\HttpClient\Response\AmpResponseV5>, but Symfony\Contracts\HttpClient\ResponseInterface|iterable<array-key, Symfony\Contracts\HttpClient\ResponseInterface>|list{Symfony\Component\HttpClient\Response\AmpResponseV4|Symfony\Component\HttpClient\Response\AmpResponseV5} provided (see https://psalm.dev/004)
}

return new ResponseStream(AmpResponseV4::stream($responses, $timeout));

Check failure on line 173 in src/Symfony/Component/HttpClient/AmpHttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidArgument

src/Symfony/Component/HttpClient/AmpHttpClient.php:173:57: InvalidArgument: Argument 1 of Symfony\Component\HttpClient\Response\AmpResponseV4::stream expects iterable<array-key, Symfony\Component\HttpClient\Response\AmpResponseV4>, but Symfony\Contracts\HttpClient\ResponseInterface|iterable<array-key, Symfony\Contracts\HttpClient\ResponseInterface>|list{Symfony\Component\HttpClient\Response\AmpResponseV4|Symfony\Component\HttpClient\Response\AmpResponseV5} provided (see https://psalm.dev/004)
}

public function reset(): void
{
$this->multi->dnsCache = [];

foreach ($this->multi->pushedResponses as $authority => $pushedResponses) {
foreach ($this->multi->pushedResponses as $pushedResponses) {
foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) {
$pushDeferred->fail(new CancelledException());
if ($pushDeferred instanceof DeferredFuture) {

Check failure on line 182 in src/Symfony/Component/HttpClient/AmpHttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/HttpClient/AmpHttpClient.php:182:46: UndefinedClass: Class, interface or enum named Amp\DeferredFuture does not exist (see https://psalm.dev/019)
$pushDeferred->error(new CancelledException());

Check failure on line 183 in src/Symfony/Component/HttpClient/AmpHttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/HttpClient/AmpHttpClient.php:183:21: UndefinedClass: Class, interface or enum named Amp\DeferredFuture does not exist (see https://psalm.dev/019)
} else {
$pushDeferred->fail(new CancelledException());
}

$this->logger?->debug(\sprintf('Unused pushed response: "%s"', $pushedUrl));
}
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/HttpClient/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.2
---

* Add support for amphp/http-client v5 on PHP 8.4+

7.1
---

Expand Down
6 changes: 3 additions & 3 deletions src/Symfony/Component/HttpClient/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

namespace Symfony\Component\HttpClient;

use Amp\Http\Client\Connection\ConnectionLimitingPool;
use Amp\Promise;
use Amp\Http\Client\Request as AmpRequest;
use Amp\Http\HttpMessage;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
Expand All @@ -31,7 +31,7 @@
*/
public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
{
if ($amp = class_exists(ConnectionLimitingPool::class) && interface_exists(Promise::class)) {
if ($amp = class_exists(AmpRequest::class) && (\PHP_VERSION_ID >= 80400 || is_subclass_of(AmpRequest::class, HttpMessage::class))) {

Check failure on line 34 in src/Symfony/Component/HttpClient/HttpClient.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/HttpClient/HttpClient.php:34:118: UndefinedClass: Class, interface or enum named Amp\Http\HttpMessage does not exist (see https://psalm.dev/019)
if (!\extension_loaded('curl')) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*
* @internal
*/
class AmpBody implements RequestBody, InputStream
class AmpBodyV4 implements RequestBody, InputStream
{
private ResourceInputStream|\Closure|string $body;
private array $info;
Expand Down
150 changes: 150 additions & 0 deletions src/Symfony/Component/HttpClient/Internal/AmpBodyV5.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\ReadableBuffer;
use Amp\ByteStream\ReadableIterableStream;
use Amp\ByteStream\ReadableResourceStream;
use Amp\ByteStream\ReadableStream;
use Amp\Cancellation;
use Amp\Http\Client\HttpContent;
use Symfony\Component\HttpClient\Exception\TransportException;

/**
* @author Nicolas Grekas <[email protected]>
*
* @internal
*/
class AmpBodyV5 implements HttpContent, ReadableStream, \IteratorAggregate

Check failure on line 27 in src/Symfony/Component/HttpClient/Internal/AmpBodyV5.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/HttpClient/Internal/AmpBodyV5.php:27:28: UndefinedClass: Class, interface or enum named Amp\Http\Client\HttpContent does not exist (see https://psalm.dev/019)
{
private ReadableStream $body;
private ?string $content;
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 ReadableResourceStream($body);
} elseif (\is_string($body)) {
$this->length = \strlen($body);
$this->body = new ReadableBuffer($body);
$this->content = $body;
} else {
$this->body = new ReadableIterableStream((static function () use ($body) {
while ('' !== $data = ($body)(16372)) {
if (!\is_string($data)) {
throw new TransportException(\sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
}

yield $data;
}
})());
}
}

public function getContent(): ReadableStream
{
if (null !== $this->uploaded) {
$this->uploaded = null;

if (\is_string($this->body)) {
$this->offset = 0;
} elseif ($this->body instanceof ReadableResourceStream) {
fseek($this->body->getResource(), $this->offset);
}
}

return $this;
}

public function getContentType(): ?string
{
return null;
}

public function getContentLength(): ?int
{
return 0 <= $this->length ? $this->length - $this->offset : null;
}

public function read(?Cancellation $cancellation = null): ?string
{
$this->info['size_upload'] += $this->uploaded;
$this->uploaded = 0;
($this->onProgress)();

if (null !== $data = $this->body->read($cancellation)) {
$this->uploaded = \strlen($data);
} else {
$this->info['upload_content_length'] = $this->info['size_upload'];
}

return $data;
}

public function isReadable(): bool
{
return $this->body->isReadable();
}

public function close(): void
{
$this->body->close();
}

public function isClosed(): bool
{
return $this->body->isClosed();
}

public function onClose(\Closure $onClose): void
{
$this->body->onClose($onClose);
}

public function getIterator(): \Traversable
{
return $this->body;
}

public static function rewind(HttpContent $body): HttpContent
{
if (!$body instanceof self) {
return $body;
}

$body->uploaded = null;

if ($body->body instanceof ReadableResourceStream && !$body->body->isClosed()) {
fseek($body->body->getResource(), $body->offset);
}

if ($body->body instanceof ReadableBuffer) {
return new $body($body->content, $body->info, $body->onProgress);
}

return $body;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
*
* @internal
*/
final class AmpClientState extends ClientState
final class AmpClientStateV4 extends ClientState
{
public array $dnsCache = [];
public int $responseCount = 0;
Expand Down Expand Up @@ -90,13 +90,13 @@
$info['peer_certificate_chain'] = [];
}

$request->addEventListener(new AmpListener($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle));
$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;

Check failure on line 99 in src/Symfony/Component/HttpClient/Internal/AmpClientStateV4.php

View workflow job for this annotation

GitHub Actions / Psalm

InvalidArgument

src/Symfony/Component/HttpClient/Internal/AmpClientStateV4.php:99:56: InvalidArgument: Isset only works with variables and array elements (see https://psalm.dev/004)
}
});

Expand Down Expand Up @@ -157,7 +157,7 @@
return $result;
}
};
$connector->connector = new DnsConnector(new AmpResolver($this->dnsCache));
$connector->connector = new DnsConnector(new AmpResolverV4($this->dnsCache));

$context = (new ConnectContext())
->withTcpNoDelay()
Expand Down
Loading
Loading