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
- 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).
- Install ca-certificates on the OS (apt-get install ca-certificates / apk add ca-certificates) and use the system path.
- 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
- Ship cacert.pem in your artifact/image or install the OS ca-certificates package.
- Use absolute paths; verify at startup with is_readable().
- Default to the system bundle via openssl.cafile/openssl.capath ini settings.
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
- SSL CA bundle not found: %s
- SSL certificate not found: %s
- SSL private key not found: %s
- Passing the "stream_context" request option to a cURL handle
- %s must be a non-empty string
AI-assisted analysis of guzzle/guzzle@9b200fc580 (2026-08-04).
Data as JSON: /data/errors/253669b89c0b6cf1.json.
Report an issue: GitHub.