From 3491986ad3b529b0b6ea291093d099d3b121981f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jan 2024 17:13:54 +0100 Subject: [PATCH] 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 --- doc/03-cli.md | 5 +++++ doc/articles/troubleshooting.md | 5 +++++ phpstan/baseline.neon | 12 +--------- src/Composer/Command/DiagnoseCommand.php | 7 +++++- src/Composer/Util/Http/CurlDownloader.php | 27 ++++++++++++++++++----- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index a98a7d629..6e6809a49 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -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 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 If you use a proxy, but it does not support the request_fulluri flag, then you diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index 7d5afdea9..0607535d7 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -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 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:** On linux, it seems that running this command helps to make ipv4 traffic have a diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index 50f63ff25..b17166a49 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -765,16 +765,6 @@ parameters: count: 1 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Only booleans are allowed in \\|\\|, array\\ given on the left side\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - - - message: "#^Only booleans are allowed in \\|\\|, array\\ 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\\\\>, array\\\\>\\|string\\|null\\>\\> given\\.$#" count: 2 @@ -4314,7 +4304,7 @@ parameters: path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\, 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\\, 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\\, 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 path: ../src/Composer/Util/Http/CurlDownloader.php diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 7d41bf554..18b1911d7 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -553,7 +553,7 @@ EOT if ($result) { foreach ($result as $message) { - $io->write($message); + $io->write(trim($message)); } } } @@ -776,6 +776,11 @@ EOT $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; } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 967aed2e7..f5bbe24aa 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -28,7 +28,7 @@ use React\Promise\Promise; * @internal * @author Jordi Boggiano * @author Nicolas Grekas - * @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} */ class CurlDownloader @@ -143,7 +143,7 @@ class CurlDownloader /** * @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 * * @return int internal job id @@ -155,8 +155,15 @@ class CurlDownloader 'redirects' => 0, 'retries' => 0, 'storeAuth' => false, + 'ipResolve' => null, ], $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; // 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_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')) { 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')) ) && $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->restartJobWithDelay($job, $job['url'], ['retries' => $job['attributes']['retries'] + 1]); + $this->restartJobWithDelay($job, $job['url'], $attributes); continue; } @@ -582,7 +599,7 @@ class CurlDownloader * @param Job $job * @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 { @@ -600,7 +617,7 @@ class CurlDownloader * @param Job $job * @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 {