Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 96b3fc5

Browse filesBrowse files
[HttpClient] Add portable HTTP/2 implementation based on Amp's HTTP client
1 parent 392d0b0 commit 96b3fc5
Copy full SHA for 96b3fc5

File tree

9 files changed

+797
-11
lines changed
Filter options

9 files changed

+797
-11
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"symfony/yaml": "self.version"
100100
},
101101
"require-dev": {
102+
"amphp/http-client": "^4.0",
102103
"cache/integration-tests": "dev-master",
103104
"doctrine/annotations": "~1.0",
104105
"doctrine/cache": "~1.6",
+162Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Amp\Http\Client\HttpClientBuilder;
15+
use Amp\Http\Client\Request;
16+
use Psr\Log\LoggerAwareInterface;
17+
use Psr\Log\LoggerAwareTrait;
18+
use Symfony\Component\HttpClient\Exception\TransportException;
19+
use Symfony\Component\HttpClient\Internal\AmpClientState;
20+
use Symfony\Component\HttpClient\Response\AmpResponse;
21+
use Symfony\Component\HttpClient\Response\ResponseStream;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
use Symfony\Contracts\HttpClient\ResponseInterface;
24+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
25+
use Symfony\Contracts\Service\ResetInterface;
26+
27+
if (!class_exists(HttpClientBuilder::class)) {
28+
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".');
29+
}
30+
31+
/**
32+
* A portable implementation of the HttpClientInterface contracts based on Amp's HTTP client.
33+
*
34+
* @author Nicolas Grekas <p@tchwork.com>
35+
*/
36+
final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
37+
{
38+
use HttpClientTrait;
39+
use LoggerAwareTrait;
40+
41+
private $defaultOptions = self::OPTIONS_DEFAULTS;
42+
43+
/** @var AmpClientState */
44+
private $multi;
45+
46+
/**
47+
* @param array $defaultOptions Default requests' options
48+
* @param int $maxHostConnections The maximum number of connections to open
49+
*
50+
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
51+
*/
52+
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, HttpClientBuilder $builder = null)
53+
{
54+
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
55+
56+
if ($defaultOptions) {
57+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
58+
}
59+
60+
$this->multi = new AmpClientState($builder, $maxHostConnections);
61+
}
62+
63+
/**
64+
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
65+
*
66+
* {@inheritdoc}
67+
*/
68+
public function request(string $method, string $url, array $options = []): ResponseInterface
69+
{
70+
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
71+
72+
// TODO handle missing options:
73+
// - on_progress
74+
// - resolve
75+
// - proxy
76+
// - no_proxy
77+
// - verify_host
78+
// - passphrase
79+
// - peer_fingerprint
80+
// - capture_peer_cert_chain
81+
82+
// TODO stream the body upload when possible
83+
$options['body'] = self::getBodyAsString($options['body']);
84+
85+
if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) {
86+
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
87+
}
88+
89+
$this->logger && $this->logger->info(sprintf('Request: %s %s', $method, implode('', $url)));
90+
91+
if (!isset($options['normalized_headers']['user-agent'])) {
92+
$options['headers'][] = 'User-Agent: Symfony HttpClient/Amp';
93+
}
94+
95+
if (0 < $options['max_duration']) {
96+
$options['timeout'] = min($options['max_duration'], $options['timeout']);
97+
}
98+
99+
$request = new Request(implode('', $url), $method);
100+
101+
if ($options['http_version']) {
102+
switch ((float) $options['http_version']) {
103+
case 1.0: $request->setProtocolVersions(['1.0']); break;
104+
case 1.1: $request->setProtocolVersions(['1.1', '1.0']); break;
105+
default: $request->setProtocolVersions(['2', '1.1', '1.0']); break;
106+
}
107+
}
108+
109+
foreach ($options['headers'] as $v) {
110+
$h = explode(': ', $v, 2);
111+
$request->addHeader($h[0], $h[1]);
112+
}
113+
114+
$request->setTcpConnectTimeout(1000 * $options['timeout']);
115+
$request->setTlsHandshakeTimeout(1000 * $options['timeout']);
116+
$request->setTransferTimeout(1000 * $options['max_duration']);
117+
$request->setBody($options['body'] ?? '');
118+
119+
return new AmpResponse($this->multi, $request, $options, $this->logger);
120+
}
121+
122+
/**
123+
* {@inheritdoc}
124+
*/
125+
public function stream($responses, float $timeout = null): ResponseStreamInterface
126+
{
127+
if ($responses instanceof AmpResponse) {
128+
$responses = [$responses];
129+
} elseif (!is_iterable($responses)) {
130+
throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of AmpResponse objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses)));
131+
}
132+
133+
return new ResponseStream(AmpResponse::stream($responses, $timeout));
134+
}
135+
136+
public function reset()
137+
{
138+
}
139+
140+
private static function getBodyAsString($body): string
141+
{
142+
if (\is_resource($body)) {
143+
return stream_get_contents($body);
144+
}
145+
146+
if (!$body instanceof \Closure) {
147+
return $body;
148+
}
149+
150+
$result = '';
151+
152+
while ('' !== $data = $body(self::$CHUNK_SIZE)) {
153+
if (!\is_string($data)) {
154+
throw new TransportException(sprintf('Return value of the "body" option callback must be string, %s returned.', \gettype($data)));
155+
}
156+
157+
$result .= $data;
158+
}
159+
160+
return $result;
161+
}
162+
}

‎src/Symfony/Component/HttpClient/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.1.0
5+
-----
6+
7+
* added `AmpHttpClient`, a portable HTTP/2 implementation based on Amp
8+
49
4.4.0
510
-----
611

+88Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Internal;
13+
14+
use Amp\Http\Client\Connection\DefaultConnectionFactory;
15+
use Amp\Http\Client\Connection\LimitedConnectionPool;
16+
use Amp\Http\Client\Connection\UnlimitedConnectionPool;
17+
use Amp\Http\Client\HttpClient;
18+
use Amp\Http\Client\HttpClientBuilder;
19+
use Amp\Socket\Certificate;
20+
use Amp\Socket\ClientTlsContext;
21+
use Amp\Socket\ConnectContext;
22+
use Amp\Socket\DnsConnector;
23+
use Amp\Socket\StaticConnector;
24+
use Amp\Sync\LocalKeyedSemaphore;
25+
26+
/**
27+
* Internal representation of the Amp client's state.
28+
*
29+
* @author Nicolas Grekas <p@tchwork.com>
30+
*
31+
* @internal
32+
*/
33+
final class AmpClientState extends ClientState
34+
{
35+
/** @var HttpClientBuilder[] */
36+
private $clients = [];
37+
private $maxHostConnections = 0;
38+
private $builder;
39+
40+
public function __construct(?HttpClientBuilder $builder, int $maxHostConnections)
41+
{
42+
$this->builder = ($builder ?? (new HttpClientBuilder())->allowDeprecatedUriUserInfo())->followRedirects(0);
43+
$this->maxHostConnections = $maxHostConnections;
44+
}
45+
46+
public function getClient(array $options): HttpClient
47+
{
48+
$options = [
49+
'bindto' => $options['bindto'] ?: '0',
50+
'verify_peer' => $options['verify_peer'],
51+
'capath' => $options['capath'],
52+
'cafile' => $options['cafile'],
53+
'local_cert' => $options['local_cert'],
54+
'local_pk' => $options['local_pk'],
55+
'ciphers' => $options['ciphers'],
56+
'capture_peer_cert_chain' => $options['capture_peer_cert_chain'] || $options['peer_fingerprint'],
57+
];
58+
59+
$key = implode("\0", $options);
60+
61+
if (isset($this->clients[$key])) {
62+
return $this->clients[$key];
63+
}
64+
65+
$context = new ClientTlsContext('');
66+
$options['verify_peer'] || $context = $context->withoutPeerVerification();
67+
$options['cafile'] && $context = $context->withCaFile($options['cafile']);
68+
$options['capath'] && $context = $context->withCaPath($options['capath']);
69+
$options['local_cert'] && $context = $context->withCertificate(new Certificate($options['local_cert'], $options['local_pk']));
70+
$options['ciphers'] && $context = $context->withCiphers($options['ciphers']);
71+
$options['capture_peer_cert_chain'] && $context = $context->withPeerCapturing();
72+
73+
if ($options['bindto']) {
74+
$connector = (file_exists($options['bindto']) ? 'unix://' : 'tcp://').$options['bindto'];
75+
$connector = new StaticConnector($connector, new DnsConnector());
76+
} else {
77+
$connector = null;
78+
}
79+
80+
$pool = new UnlimitedConnectionPool(new DefaultConnectionFactory($connector, (new ConnectContext())->withTlsContext($context)));
81+
82+
if (0 < $this->maxHostConnections) {
83+
$pool = LimitedConnectionPool::byHost($pool, new LocalKeyedSemaphore($this->maxHostConnections));
84+
}
85+
86+
return $this->clients[$key] = $this->builder->usingPool($pool)->build();
87+
}
88+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.