guzzle/guzzle · error · GuzzleHttp\Exception\InvalidArgumentException

SSL CA bundle not found: %s

Error message

SSL CA bundle not found: %s

What it means

Thrown in applyHandlerOptions() when the 'verify' option is a non-empty string used as a CA bundle path but file_exists() reports it does not exist as a file, directory, or symlink. Guzzle distinguishes file (CURLOPT_CAINFO) vs directory (CURLOPT_CAPATH) CA stores and validates the path exists before handing it to cURL. The %s is the escaped user-supplied path via Psr7\DiagnosticValue::escape.

Source

Thrown at src/Handler/CurlFactory.php:2468

    private function applyHandlerOptions(
        #[\SensitiveParameter]
        EasyHandle $easy,
        #[\SensitiveParameter]
        array &$conf
    ): void {
        $options = $easy->options;
        if (isset($options['verify'])) {
            if ($options['verify'] === false) {
                unset($conf[\CURLOPT_CAINFO]);
                $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
                $conf[\CURLOPT_SSL_VERIFYPEER] = false;
            } else {
                $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
                $conf[\CURLOPT_SSL_VERIFYPEER] = true;
                if (\is_string($options['verify'])) {
                    // Throw an error if the file/folder/link path is not valid or doesn't exist.
                    if (!\file_exists($options['verify'])) {
                        throw new InvalidArgumentException(\sprintf('SSL CA bundle not found: %s', Psr7\DiagnosticValue::escape($options['verify'])));
                    }
                    // If it's a directory or a link to a directory use CURLOPT_CAPATH.
                    // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
                    if (
                        \is_dir($options['verify'])
                        || (
                            \is_link($options['verify']) === true
                            && ($verifyLink = \readlink($options['verify'])) !== false
                            && \is_dir($verifyLink)
                        )
                    ) {
                        $conf[\CURLOPT_CAPATH] = $options['verify'];
                    } else {
                        $conf[\CURLOPT_CAINFO] = $options['verify'];
                    }
                }
            }
        }

View on GitHub (pinned to 9b200fc580)

Solutions

  1. Point 'verify' at an existing CA bundle file or directory (e.g. download https://curl.se/ca/cacert.pem, or use the system directory like /etc/ssl/certs).
  2. Install ca-certificates on the OS (apt-get install ca-certificates / apk add ca-certificates) and use the system path.
  3. If you intentionally do not want verification, set 'verify' => false explicitly (not recommended for production).

Example fix

// before
['verify' => '/app/config/cacert.pem'] // file not shipped in image
// after (ship the file) or use the system bundle
['verify' => ini_get('openssl.cafile') ?: '/etc/ssl/certs/ca-certificates.crt']
Defensive patterns

Strategy: validation

Validate before calling

if (isset($options['verify']) && is_string($options['verify']) && !file_exists($options['verify'])) {
    throw new InvalidArgumentException('SSL CA bundle not found: ' . $options['verify']);
}

Type guard

function caBundleExists($verify): bool {
    return !is_string($verify) || file_exists($verify);
}

Prevention

When it happens

Trigger: Setting ['verify' => '/etc/ssl/cacert.pem'] (or a directory) where the path does not exist on the host; relative paths resolved against an unexpected cwd; deploy missing the bundled CA file.

Common situations: Bundling a cacert.pem that was not shipped to the container; environment-specific paths (different on dev vs prod); typo in the path; running in a minimal Alpine image without ca-certificates installed.

Related errors


AI-assisted analysis of guzzle/guzzle@9b200fc580 (2026-08-04). Data as JSON: /data/errors/253669b89c0b6cf1.json. Report an issue: GitHub.