1
0
Fork 0

Add IPv4 fallback on connection timeout, and adds COMPOSER_IPRESOLVE env var (#11791)

* Add IPv4 fallback on connection timeout, and adds COMPOSER_IPRESOLVE env var, fixes #530

* Address feedback

* Add warning in diagnose command when COMPOSER_IPRESOLVE is set
pull/11794/head
Jordi Boggiano 2024-01-11 17:13:54 +01:00 committed by GitHub
parent c069174ac7
commit 3491986ad3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 39 additions and 17 deletions

View File

@ -1244,6 +1244,11 @@ defaults to 12 and must be between 1 and 50. If your proxy has issues with
concurrency maybe you want to lower this. Increasing it should generally not result concurrency maybe you want to lower this. Increasing it should generally not result
in performance gains. in performance gains.
### COMPOSER_IPRESOLVE
Set to `4` or `6` to force IPv4 or IPv6 DNS resolution. This only works when the
curl extension is used for downloads.
### HTTP_PROXY_REQUEST_FULLURI ### HTTP_PROXY_REQUEST_FULLURI
If you use a proxy, but it does not support the request_fulluri flag, then you If you use a proxy, but it does not support the request_fulluri flag, then you

View File

@ -304,6 +304,11 @@ open stream: Operation timed out
We recommend you fix your IPv6 setup. If that is not possible, you can try the We recommend you fix your IPv6 setup. If that is not possible, you can try the
following workarounds: following workarounds:
**Generic Workaround:**
Set the [`COMPOSER_IPRESOLVE=4`](../03-cli.md#composer-ipresolve) environment variable which will force curl to resolve
domains using IPv4. This only works when the curl extension is used for downloads.
**Workaround Linux:** **Workaround Linux:**
On linux, it seems that running this command helps to make ipv4 traffic have a On linux, it seems that running this command helps to make ipv4 traffic have a

View File

@ -765,16 +765,6 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Command/ShowCommand.php path: ../src/Composer/Command/ShowCommand.php
-
message: "#^Only booleans are allowed in \\|\\|, array\\<string, Composer\\\\Package\\\\Link\\> given on the left side\\.$#"
count: 1
path: ../src/Composer/Command/ShowCommand.php
-
message: "#^Only booleans are allowed in \\|\\|, array\\<string, Composer\\\\Package\\\\Link\\> given on the right side\\.$#"
count: 1
path: ../src/Composer/Command/ShowCommand.php
- -
message: "#^Parameter \\#1 \\$arrayTree of method Composer\\\\Command\\\\ShowCommand\\:\\:displayPackageTree\\(\\) expects array\\<int, array\\<string, array\\|string\\>\\>, array\\<int, array\\<string, array\\<int, array\\<string, array\\|string\\>\\>\\|string\\|null\\>\\> given\\.$#" message: "#^Parameter \\#1 \\$arrayTree of method Composer\\\\Command\\\\ShowCommand\\:\\:displayPackageTree\\(\\) expects array\\<int, array\\<string, array\\|string\\>\\>, array\\<int, array\\<string, array\\<int, array\\<string, array\\|string\\>\\>\\|string\\|null\\>\\> given\\.$#"
count: 2 count: 2
@ -4314,7 +4304,7 @@ parameters:
path: ../src/Composer/Util/Http/CurlDownloader.php path: ../src/Composer/Util/Http/CurlDownloader.php
- -
message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#" message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#"
count: 1 count: 1
path: ../src/Composer/Util/Http/CurlDownloader.php path: ../src/Composer/Util/Http/CurlDownloader.php

View File

@ -553,7 +553,7 @@ EOT
if ($result) { if ($result) {
foreach ($result as $message) { foreach ($result as $message) {
$io->write($message); $io->write(trim($message));
} }
} }
} }
@ -776,6 +776,11 @@ EOT
$out($iniMessage, 'comment'); $out($iniMessage, 'comment');
} }
if (in_array(Platform::getEnv('COMPOSER_IPRESOLVE'), ['4', '6'], true)) {
$warnings['ipresolve'] = true;
$out('The COMPOSER_IPRESOLVE env var is set to ' . Platform::getEnv('COMPOSER_IPRESOLVE') .' which may result in network failures below.', 'comment');
}
return count($warnings) === 0 && count($errors) === 0 ? true : $output; return count($warnings) === 0 && count($errors) === 0 ? true : $output;
} }

View File

@ -28,7 +28,7 @@ use React\Promise\Promise;
* @internal * @internal
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
* @author Nicolas Grekas <p@tchwork.com> * @author Nicolas Grekas <p@tchwork.com>
* @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool} * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool, ipResolve: 4|6|null}
* @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable} * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable}
*/ */
class CurlDownloader class CurlDownloader
@ -143,7 +143,7 @@ class CurlDownloader
/** /**
* @param mixed[] $options * @param mixed[] $options
* *
* @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, storeAuth?: 'prompt'|bool} $attributes * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, storeAuth?: 'prompt'|bool, ipResolve?: 4|6|null} $attributes
* @param non-empty-string $url * @param non-empty-string $url
* *
* @return int internal job id * @return int internal job id
@ -155,8 +155,15 @@ class CurlDownloader
'redirects' => 0, 'redirects' => 0,
'retries' => 0, 'retries' => 0,
'storeAuth' => false, 'storeAuth' => false,
'ipResolve' => null,
], $attributes); ], $attributes);
if ($attributes['ipResolve'] === null && Platform::getEnv('COMPOSER_IPRESOLVE') === '4') {
$attributes['ipResolve'] = 4;
} elseif ($attributes['ipResolve'] === null && Platform::getEnv('COMPOSER_IPRESOLVE') === '6') {
$attributes['ipResolve'] = 6;
}
$originalOptions = $options; $originalOptions = $options;
// check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 // check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256
@ -199,6 +206,12 @@ class CurlDownloader
curl_setopt($curlHandle, CURLOPT_ENCODING, ""); // let cURL set the Accept-Encoding header to what it supports curl_setopt($curlHandle, CURLOPT_ENCODING, ""); // let cURL set the Accept-Encoding header to what it supports
curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
if ($attributes['ipResolve'] === 4) {
curl_setopt($curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
} elseif ($attributes['ipResolve'] === 6) {
curl_setopt($curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
}
if (function_exists('curl_share_init')) { if (function_exists('curl_share_init')) {
curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle); curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle);
} }
@ -352,8 +365,12 @@ class CurlDownloader
|| (in_array($errno, [56 /* CURLE_RECV_ERROR */, 35 /* CURLE_SSL_CONNECT_ERROR */], true) && str_contains((string) $error, 'Connection reset by peer')) || (in_array($errno, [56 /* CURLE_RECV_ERROR */, 35 /* CURLE_SSL_CONNECT_ERROR */], true) && str_contains((string) $error, 'Connection reset by peer'))
) && $job['attributes']['retries'] < $this->maxRetries ) && $job['attributes']['retries'] < $this->maxRetries
) { ) {
$attributes = ['retries' => $job['attributes']['retries'] + 1];
if ($errno === 7 && !isset($job['attributes']['ipResolve'])) { // CURLE_COULDNT_CONNECT, retry forcing IPv4 if no IP stack was selected
$attributes['ipResolve'] = 4;
}
$this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to curl error '. $errno, true, IOInterface::DEBUG); $this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to curl error '. $errno, true, IOInterface::DEBUG);
$this->restartJobWithDelay($job, $job['url'], ['retries' => $job['attributes']['retries'] + 1]); $this->restartJobWithDelay($job, $job['url'], $attributes);
continue; continue;
} }
@ -582,7 +599,7 @@ class CurlDownloader
* @param Job $job * @param Job $job
* @param non-empty-string $url * @param non-empty-string $url
* *
* @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>} $attributes * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>, ipResolve?: 4|6} $attributes
*/ */
private function restartJob(array $job, string $url, array $attributes = []): void private function restartJob(array $job, string $url, array $attributes = []): void
{ {
@ -600,7 +617,7 @@ class CurlDownloader
* @param Job $job * @param Job $job
* @param non-empty-string $url * @param non-empty-string $url
* *
* @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>} $attributes * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>, ipResolve?: 4|6} $attributes
*/ */
private function restartJobWithDelay(array $job, string $url, array $attributes): void private function restartJobWithDelay(array $job, string $url, array $attributes): void
{ {