composer/composer · error · \RuntimeException

Failed getting info from curl handle

Error message

Failed getting info from curl handle {$i} ({url})

What it means

During CurlDownloader::tick(), after curl_multi_info_read reports a finished job, `curl_getinfo($curlHandle)` returns false, meaning the handle is no longer queryable. Composer throws because it cannot recover the HTTP status/timing info needed to build the response.

Solutions

  1. Upgrade the PHP curl extension and libcurl (`apt-get install --only-upgrade php-curl libcurl4`).
  2. Run `composer self-update --2` to get the latest downloader fixes.
  3. If reproducible, rule out an HTTP/2 stream bug by forcing HTTP/1.1 (avoid HTTP2-capable libcurl or set COMPOSER_IPRESOLVE).
  4. Report upstream with the output of `php -i | grep -i cURL` if it persists.

Example fix

// before
RUN apt-get install -y php8.1-curl=8.1.0  # old libcurl

// after
RUN apt-get install -y --only-upgrade php8.1-curl libcurl4
Defensive patterns

Strategy: retry

Validate before calling

// Sanity-check curl health before bulk downloads
$h = curl_init('http://127.0.0.1');
$info = $h !== false ? @curl_getinfo($h) : false;
if ($info === false) {
    throw new \RuntimeException('curl extension is broken; upgrade libcurl/php-curl.');
}

Try / catch

try {
    $downloader->tick();
} catch (\RuntimeException $e) {
    if (str_starts_with($e->getMessage(), 'Failed getting info from curl handle')) {
        // discard this downloader, build a fresh CurlDownloader, and retry the jobs
    }
    throw $e;
}

Prevention

When it happens

Trigger: CurlDownloader::tick() processes a completed handle (src/Composer/Util/Http/CurlDownloader.php:359-361); if the underlying handle was closed/invalidated between dispatch and info-read (rare internal state issue, libcurl/PHP-curl ABI mismatch, or a curl extension bug), curl_getinfo returns false.

Common situations: Bugs in libcurl or the PHP curl extension on certain versions; handle removed concurrently; very rare — usually signals a corrupt curl state, often after a curl extension crash or build mismatch.

Related errors


AI-assisted analysis of composer/composer@c435d285c9 (2026-08-07). Data as JSON: /api/errors/35bd24fb04758d28. Report an issue: GitHub.

Appendix: source

Thrown at src/Composer/Util/Http/CurlDownloader.php:361

        $active = true;
        $this->checkCurlResult(curl_multi_exec($this->multiHandle, $active));
        if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) {
            // sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select
            usleep(150);
        }

        while ($progress = curl_multi_info_read($this->multiHandle)) {
            $curlHandle = $progress['handle'];
            $result = $progress['result'];
            $i = (int) $curlHandle;
            if (!isset($this->jobs[$i])) {
                continue;
            }

            $progress = curl_getinfo($curlHandle);
            if (false === $progress) {
                throw new \RuntimeException('Failed getting info from curl handle '.$i.' ('.Url::sanitize($this->jobs[$i]['url']).')');
            }
            $job = $this->jobs[$i];
            unset($this->jobs[$i]);
            $error = curl_error($curlHandle);
            $errno = curl_errno($curlHandle);
            curl_multi_remove_handle($this->multiHandle, $curlHandle);
            if (\PHP_VERSION_ID < 80000) {
                curl_close($curlHandle);
            }

            $headers = null;
            $statusCode = null;
            $response = null;
            try {
                // TODO progress
                if (CURLE_OK !== $errno || $error || $result !== CURLE_OK) {
                    $errno = $errno ?: $result;
                    if (!$error && function_exists('curl_strerror')) {

View on GitHub (pinned to c435d285c9)