Skip to content

Commit 7109135

Browse files
Merge branch '4.3' into 4.4
* 4.3: (26 commits) [Console] Fix #33915, Detect dimensions using mode CON if vt100 is supported [HttpKernel][DataCollectorInterface] Ease compatibility Add tests to ensure defaultLocale is properly passed to the URL generator [DependencyInjection] Fix broken references in tests [HttpClient] Retry safe requests when then fail before the body arrives Avoid using of kernel after shutdown Simplify PHP CS Fixer configuration [PropertyInfo] Fixed type extraction for nullable collections of non-nullable elements [FrameworkBundle] [HttpKernel] fixed correct EOL and EOM month [Serializer] Fix property name usage for denormalization Name test accordingly to the tested class Fix MockFileSessionStorageTest::sessionDir being used after it's unset bumped Symfony version to 4.3.7 updated VERSION for 4.3.6 updated CHANGELOG for 4.3.6 bumped Symfony version to 3.4.34 updated VERSION for 3.4.33 update CONTRIBUTORS for 3.4.33 updated CHANGELOG for 3.4.33 [HttpClient] Fix perf issue when doing thousands of requests with curl ...
2 parents a874190 + 6519e8e commit 7109135

File tree

6 files changed

+80
-33
lines changed

6 files changed

+80
-33
lines changed

Chunk/ErrorChunk.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,19 @@ class ErrorChunk implements ChunkInterface
2626
private $errorMessage;
2727
private $error;
2828

29-
public function __construct(int $offset, \Throwable $error = null)
29+
/**
30+
* @param \Throwable|string $error
31+
*/
32+
public function __construct(int $offset, $error)
3033
{
3134
$this->offset = $offset;
32-
$this->error = $error;
33-
$this->errorMessage = null !== $error ? $error->getMessage() : 'Reading from the response stream reached the idle timeout.';
35+
36+
if (\is_string($error)) {
37+
$this->errorMessage = $error;
38+
} else {
39+
$this->error = $error;
40+
$this->errorMessage = $error->getMessage();
41+
}
3442
}
3543

3644
/**

CurlHttpClient.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
4949
*/
5050
private $multi;
5151

52+
private static $curlVersion;
53+
5254
/**
5355
* @param array $defaultOptions Default requests' options
5456
* @param int $maxHostConnections The maximum number of connections to a single host
@@ -69,6 +71,7 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
6971
}
7072

7173
$this->multi = $multi = new CurlClientState();
74+
self::$curlVersion = self::$curlVersion ?? curl_version();
7275

7376
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
7477
if (\defined('CURLPIPE_MULTIPLEX')) {
@@ -87,7 +90,7 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
8790
}
8891

8992
// HTTP/2 push crashes before curl 7.61
90-
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073d00 > ($v = curl_version())['version_number'] || !(CURL_VERSION_HTTP2 & $v['features'])) {
93+
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073d00 > self::$curlVersion['version_number'] || !(CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
9194
return;
9295
}
9396

@@ -192,7 +195,7 @@ public function request(string $method, string $url, array $options = []): Respo
192195
$this->multi->dnsCache->evictions = [];
193196
$port = parse_url($authority, PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
194197

195-
if ($resolve && 0x072a00 > curl_version()['version_number']) {
198+
if ($resolve && 0x072a00 > self::$curlVersion['version_number']) {
196199
// DNS cache removals require curl 7.42 or higher
197200
// On lower versions, we have to create a new multi handle
198201
curl_multi_close($this->multi->handle);
@@ -212,7 +215,7 @@ public function request(string $method, string $url, array $options = []): Respo
212215
$curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
213216
} elseif (1.1 === (float) $options['http_version'] || 'https:' !== $scheme) {
214217
$curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
215-
} elseif (\defined('CURL_VERSION_HTTP2') && CURL_VERSION_HTTP2 & curl_version()['features']) {
218+
} elseif (\defined('CURL_VERSION_HTTP2') && CURL_VERSION_HTTP2 & self::$curlVersion['features']) {
216219
$curlopts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
217220
}
218221

Response/CurlResponse.php

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
*/
2626
final class CurlResponse implements ResponseInterface
2727
{
28-
use ResponseTrait;
28+
use ResponseTrait {
29+
getContent as private doGetContent;
30+
}
2931

3032
private static $performing = false;
3133
private $multi;
@@ -60,7 +62,7 @@ public function __construct(CurlClientState $multi, $ch, array $options = null,
6062

6163
if (!$info['response_headers']) {
6264
// Used to keep track of what we're waiting for
63-
curl_setopt($ch, CURLOPT_PRIVATE, 'headers');
65+
curl_setopt($ch, CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
6466
}
6567

6668
if (null === $content = &$this->content) {
@@ -139,7 +141,7 @@ public function __construct(CurlClientState $multi, $ch, array $options = null,
139141

140142
$waitFor = curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE);
141143

142-
if (\in_array($waitFor, ['headers', 'destruct'], true)) {
144+
if ('H' === $waitFor[0] || 'D' === $waitFor[0]) {
143145
try {
144146
foreach (self::stream([$response]) as $chunk) {
145147
if ($chunk->isFirst()) {
@@ -153,16 +155,11 @@ public function __construct(CurlClientState $multi, $ch, array $options = null,
153155
throw $e;
154156
}
155157
}
156-
157-
curl_setopt($ch, CURLOPT_HEADERFUNCTION, null);
158-
curl_setopt($ch, CURLOPT_READFUNCTION, null);
159-
curl_setopt($ch, CURLOPT_INFILE, null);
160158
};
161159

162160
// Schedule the request in a non-blocking way
163161
$multi->openHandles[$id] = [$ch, $options];
164162
curl_multi_add_handle($multi->handle, $ch);
165-
self::perform($multi);
166163
}
167164

168165
/**
@@ -171,8 +168,6 @@ public function __construct(CurlClientState $multi, $ch, array $options = null,
171168
public function getInfo(string $type = null)
172169
{
173170
if (!$info = $this->finalInfo) {
174-
self::perform($this->multi);
175-
176171
$info = array_merge($this->info, curl_getinfo($this->handle));
177172
$info['url'] = $this->info['url'] ?? $info['url'];
178173
$info['redirect_url'] = $this->info['redirect_url'] ?? null;
@@ -185,8 +180,9 @@ public function getInfo(string $type = null)
185180

186181
rewind($this->debugBuffer);
187182
$info['debug'] = stream_get_contents($this->debugBuffer);
183+
$waitFor = curl_getinfo($this->handle, CURLINFO_PRIVATE);
188184

189-
if (!\in_array(curl_getinfo($this->handle, CURLINFO_PRIVATE), ['headers', 'content'], true)) {
185+
if ('H' !== $waitFor[0] && 'C' !== $waitFor[0]) {
190186
curl_setopt($this->handle, CURLOPT_VERBOSE, false);
191187
rewind($this->debugBuffer);
192188
ftruncate($this->debugBuffer, 0);
@@ -197,17 +193,35 @@ public function getInfo(string $type = null)
197193
return null !== $type ? $info[$type] ?? null : $info;
198194
}
199195

196+
/**
197+
* {@inheritdoc}
198+
*/
199+
public function getContent(bool $throw = true): string
200+
{
201+
$performing = self::$performing;
202+
self::$performing = $performing || '_0' === curl_getinfo($this->handle, CURLINFO_PRIVATE);
203+
204+
try {
205+
return $this->doGetContent($throw);
206+
} finally {
207+
self::$performing = $performing;
208+
}
209+
}
210+
200211
public function __destruct()
201212
{
202213
try {
203214
if (null === $this->timeout) {
204215
return; // Unused pushed response
205216
}
206217

207-
if ('content' === $waitFor = curl_getinfo($this->handle, CURLINFO_PRIVATE)) {
218+
$waitFor = curl_getinfo($this->handle, CURLINFO_PRIVATE);
219+
220+
if ('C' === $waitFor[0] || '_' === $waitFor[0]) {
208221
$this->close();
209-
} elseif ('headers' === $waitFor) {
210-
curl_setopt($this->handle, CURLOPT_PRIVATE, 'destruct');
222+
} elseif ('H' === $waitFor[0]) {
223+
$waitFor[0] = 'D'; // D = destruct
224+
curl_setopt($this->handle, CURLOPT_PRIVATE, $waitFor);
211225
}
212226

213227
$this->doDestruct();
@@ -245,7 +259,7 @@ private function close(): void
245259
unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]);
246260
curl_multi_remove_handle($this->multi->handle, $this->handle);
247261
curl_setopt_array($this->handle, [
248-
CURLOPT_PRIVATE => '',
262+
CURLOPT_PRIVATE => '_0',
249263
CURLOPT_NOPROGRESS => true,
250264
CURLOPT_PROGRESSFUNCTION => null,
251265
CURLOPT_HEADERFUNCTION => null,
@@ -266,7 +280,7 @@ private static function schedule(self $response, array &$runningResponses): void
266280
$runningResponses[$i] = [$response->multi, [$response->id => $response]];
267281
}
268282

269-
if ('' === curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE)) {
283+
if ('_0' === curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE)) {
270284
// Response already completed
271285
$response->multi->handlesActivity[$response->id][] = null;
272286
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
@@ -294,8 +308,26 @@ private static function perform(CurlClientState $multi, array &$responses = null
294308
while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active));
295309

296310
while ($info = curl_multi_info_read($multi->handle)) {
297-
$multi->handlesActivity[(int) $info['handle']][] = null;
298-
$multi->handlesActivity[(int) $info['handle']][] = \in_array($info['result'], [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || (\CURLE_WRITE_ERROR === $info['result'] && 'destruct' === @curl_getinfo($info['handle'], CURLINFO_PRIVATE)) ? null : new TransportException(sprintf('%s for "%s".', curl_strerror($info['result']), curl_getinfo($info['handle'], CURLINFO_EFFECTIVE_URL)));
311+
$result = $info['result'];
312+
$id = (int) $ch = $info['handle'];
313+
$waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE) ?: '_0';
314+
315+
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
316+
curl_multi_remove_handle($multi->handle, $ch);
317+
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
318+
curl_setopt($ch, CURLOPT_PRIVATE, $waitFor);
319+
320+
if ('1' === $waitFor[1]) {
321+
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
322+
}
323+
324+
if (0 === curl_multi_add_handle($multi->handle, $ch)) {
325+
continue;
326+
}
327+
}
328+
329+
$multi->handlesActivity[$id][] = null;
330+
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(sprintf('%s for "%s".', curl_strerror($result), curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)));
299331
}
300332
} finally {
301333
self::$performing = false;
@@ -320,7 +352,9 @@ private static function select(CurlClientState $multi, float $timeout): int
320352
*/
321353
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger, &$content = null): int
322354
{
323-
if (!\in_array($waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE), ['headers', 'destruct'], true)) {
355+
$waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE) ?: '_0';
356+
357+
if ('H' !== $waitFor[0] && 'D' !== $waitFor[0]) {
324358
return \strlen($data); // Ignore HTTP trailers
325359
}
326360

@@ -381,14 +415,18 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
381415
}
382416

383417
if ($statusCode < 300 || 400 <= $statusCode || null === $location || curl_getinfo($ch, CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
384-
// Headers and redirects completed, time to get the response's body
418+
// Headers and redirects completed, time to get the response's content
385419
$multi->handlesActivity[$id][] = new FirstChunk();
386420

387-
if ('destruct' === $waitFor) {
388-
return 0;
421+
if ('D' === $waitFor[0] || 'HEAD' === $info['http_method'] || \in_array($statusCode, [204, 304], true)) {
422+
$waitFor = '_0'; // no content expected
423+
$multi->handlesActivity[$id][] = null;
424+
$multi->handlesActivity[$id][] = null;
425+
} else {
426+
$waitFor[0] = 'C'; // C = content
389427
}
390428

391-
curl_setopt($ch, CURLOPT_PRIVATE, 'content');
429+
curl_setopt($ch, CURLOPT_PRIVATE, $waitFor);
392430

393431
try {
394432
if (!$content && $options['buffer'] instanceof \Closure && $content = $options['buffer']($headers) ?: null) {

Response/MockResponse.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ private static function readResponse(self $response, array $options, ResponseInt
291291
foreach ($body as $chunk) {
292292
if ('' === $chunk = (string) $chunk) {
293293
// simulate an idle timeout
294-
$response->body[] = new ErrorChunk($offset);
294+
$response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
295295
} else {
296296
$response->body[] = $chunk;
297297
$offset += \strlen($chunk);

Response/NativeResponse.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ public function __construct(NativeClientState $multi, $context, string $url, $op
8282
public function getInfo(string $type = null)
8383
{
8484
if (!$info = $this->finalInfo) {
85-
self::perform($this->multi);
86-
8785
$info = $this->info;
8886
$info['url'] = implode('', $info['url']);
8987
unset($info['size_body'], $info['request_header']);

Response/ResponseTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ public static function stream(iterable $responses, float $timeout = null): \Gene
315315
unset($responses[$j]);
316316
continue;
317317
} elseif ($isTimeout) {
318-
$multi->handlesActivity[$j] = [new ErrorChunk($response->offset)];
318+
$multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
319319
} else {
320320
continue;
321321
}

0 commit comments

Comments
 (0)