J’ai récemment utilisé l’API de YouTube pour récupérer plusieurs informations sur des vidéos, afin d’ajouter au site les données structurées adéquates.
En local, lorsque l’on utilise file_get_contents() sur une URL HTTPS, on peut parfois obtenir une erreur SSL si PHP ne trouve pas le bon bundle de certificats d’autorités de certification.
L’erreur ressemble souvent à ceci :
Warning: file_get_contents(): SSL operation failed with code 1.
OpenSSL Error messages:
error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed in ...php on line 2
Warning: file_get_contents(): Failed to enable crypto in ...php on line 2
Warning: file_get_contents(https:Code language: JavaScript (javascript)
Le message peut varier selon la version de PHP, OpenSSL, le système, le conteneur Docker ou l’environnement local. Mais l’idée reste la même : PHP tente de vérifier le certificat TLS du serveur distant, et cette vérification échoue.
Depuis PHP 5.6, les flux chiffrés vérifient les certificats et les noms d’hôte par défaut. En théorie, c’est une très bonne chose. En pratique, cela expose vite une configuration locale incomplète, notamment quand le bundle CA est absent, obsolète ou mal déclaré. Documentation PHP : changements OpenSSL depuis PHP 5.6
Pourquoi cette erreur SSL apparaît
Lorsque PHP récupère une ressource HTTPS avec file_get_contents(), il utilise les streams PHP et OpenSSL. Pour accepter la connexion, PHP doit pouvoir vérifier que le certificat du site distant a été signé par une autorité de certification reconnue.
Si PHP ne trouve pas le fichier contenant les certificats racines, la vérification échoue. La requête est alors bloquée, même si l’URL fonctionne parfaitement dans votre navigateur.
Les causes les plus fréquentes sont :
- un bundle CA absent ou trop ancien ;
- une directive
openssl.cafile non configurée ;
- une directive
curl.cainfo non configurée pour cURL ;
- un environnement local ou Docker minimaliste ;
- une vieille version d’OpenSSL ;
- un proxy, antivirus ou firewall qui intercepte HTTPS ;
- un certificat distant réellement mal configuré.
Sur un serveur Linux bien configuré, le système fournit généralement les certificats racines. Sur un environnement local, un conteneur Docker ou une installation PHP manuelle, il faut parfois les déclarer explicitement.
Méthode 1 : vérifier le fichier php.ini réellement utilisé
Avant de modifier la configuration, il faut vérifier quel fichier php.ini PHP utilise réellement.
En ligne de commande :
php --ini
Vous devriez obtenir quelque chose comme :
Loaded Configuration File: /etc/php/8.3/cli/php.iniCode language: JavaScript (javascript)
Attention : PHP en ligne de commande et PHP-FPM n’utilisent pas forcément le même fichier php.ini.
Pour un site web servi via PHP-FPM, le fichier peut plutôt être :
/etc/php/8.3/fpm/php.ini
Ou, selon la version :
/etc/php/8.4/fpm/php.ini
On peut aussi vérifier les valeurs OpenSSL directement avec PHP :
php -i | grep -E "openssl.cafile|openssl.capath|curl.cainfo"Code language: JavaScript (javascript)
Si les valeurs sont vides, PHP dépendra de la configuration par défaut du système. Cela peut suffire sur un serveur bien installé, mais échouer en local ou dans un conteneur.
Méthode 2 : installer ou mettre à jour le bundle CA du système
Sur Debian ou Ubuntu, commencez par vérifier que le paquet ca-certificates est installé :
sudo apt update
sudo apt install ca-certificates
sudo update-ca-certificates
Sur Alpine Linux, souvent utilisé dans des images Docker légères :
apk add --no-cache ca-certificates
update-ca-certificates
Sur macOS avec Homebrew, le chemin peut varier, mais le bundle OpenSSL se trouve souvent dans un répertoire Homebrew :
brew install openssl ca-certificates
Ensuite, il faut indiquer à PHP où trouver le fichier CA, si PHP ne le détecte pas automatiquement.
Méthode 3 : configurer openssl.cafile dans php.ini
La directive openssl.cafile indique à PHP le chemin du fichier contenant les certificats d’autorités de certification. La documentation PHP précise que cette option sert à définir le fichier CA utilisé avec verify_peer pour authentifier le serveur distant. Documentation PHP : configuration OpenSSL
Sur Debian ou Ubuntu, le chemin courant est souvent :
/etc/ssl/certs/ca-certificates.crt
Dans php.ini, ajoutez ou adaptez :
openssl.cafile = /etc/ssl/certs/ca-certificates.crtCode language: JavaScript (javascript)
Sur macOS avec Homebrew, un chemin possible peut être :
openssl.cafile = /opt/homebrew/etc/openssl@3/cert.pemCode language: JavaScript (javascript)
Sur une ancienne installation Intel Homebrew, le chemin peut plutôt ressembler à :
openssl.cafile = /usr/local/etc/openssl/cert.pemCode language: JavaScript (javascript)
Après modification, redémarrez PHP-FPM si le problème concerne le site web :
sudo systemctl restart php8.3-fpmCode language: CSS (css)
Pour PHP en ligne de commande, relancez simplement la commande ou le terminal.
Méthode 4 : configurer curl.cainfo pour cURL
Si vous utilisez cURL au lieu de file_get_contents(), configurez aussi curl.cainfo. La documentation PHP indique que cette directive définit la valeur par défaut de l’option CURLOPT_CAINFO, et qu’elle doit être un chemin absolu. Documentation PHP : configuration cURL
curl.cainfo = /etc/ssl/certs/ca-certificates.crtCode language: JavaScript (javascript)
Dans beaucoup d’environnements, je configure les deux :
openssl.cafile = /etc/ssl/certs/ca-certificates.crt
curl.cainfo = /etc/ssl/certs/ca-certificates.crtCode language: JavaScript (javascript)
Cela couvre à la fois les streams PHP et les requêtes cURL.
Méthode 5 : utiliser Composer CA Bundle
Si vous développez une application PHP distribuée sur plusieurs environnements, le paquet Composer composer/ca-bundle peut aider à localiser un bundle CA utilisable.
Il permet notamment de détecter le fichier CA du système ou de fournir un chemin portable depuis PHP. GitHub : composer/ca-bundle
composer require composer/ca-bundleCode language: JavaScript (javascript)
Exemple avec cURL :
<?php
use Composer\CaBundle\CaBundle;
$ca_file = CaBundle::getBundledCaBundlePath();
$ch = curl_init( 'https://www.googleapis.com/youtube/v3/videos' );
curl_setopt_array(
$ch,
array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CAINFO => $ca_file,
CURLOPT_TIMEOUT => 15,
)
);
$response = curl_exec( $ch );
if ( false === $response ) {
throw new RuntimeException( curl_error( $ch ), curl_errno( $ch ) );
}
curl_close( $ch );Code language: HTML, XML (xml)
Cette approche est utile pour les librairies, les outils CLI ou les environnements locaux hétérogènes.
Méthode 6 : utiliser cURL à la place de file_get_contents()
Une autre solution consiste à remplacer file_get_contents() par une fonction basée sur cURL. Cela donne plus de contrôle : timeout, code HTTP, erreurs, user agent, redirections et certificat CA.
PHP prend en charge libcurl, qui permet de communiquer avec de nombreux protocoles, dont HTTP et HTTPS. Documentation PHP : extension cURL
Voici une version modernisée de la fonction :
<?php
declare(strict_types=1);
function sky_curl_get_file_contents( string $url, ?string $ca_file = null ): string|false {
if ( '' === trim( $url ) || false === filter_var( $url, FILTER_VALIDATE_URL ) ) {
return false;
}
$ch = curl_init( $url );
if ( false === $ch ) {
return false;
}
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_USERAGENT => 'SkyMindsBot/1.0 (+https://www.skyminds.net/)',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
);
if ( null !== $ca_file && is_readable( $ca_file ) ) {
$options[ CURLOPT_CAINFO ] = $ca_file;
}
curl_setopt_array( $ch, $options );
$body = curl_exec( $ch );
$http_code = (int) curl_getinfo( $ch, CURLINFO_RESPONSE_CODE );
curl_close( $ch );
if ( false === $body || 200 > $http_code || 300 <= $http_code ) {
return false;
}
return $body;
}Code language: HTML, XML (xml)
Exemple d’utilisation :
<?php
$response = sky_curl_get_file_contents(
'https://www.googleapis.com/youtube/v3/videos?id=VIDEO_ID&part=snippet&key=API_KEY',
'/etc/ssl/certs/ca-certificates.crt'
);
if ( false === $response ) {
return;
}
$data = json_decode( $response, true );
if ( ! is_array( $data ) ) {
return;
}Code language: HTML, XML (xml)
Cette version est déjà plus saine que la fonction initiale, car elle ajoute un timeout, vérifie le code HTTP et garde la validation SSL active.
Version cURL avec erreurs détaillées
Pour déboguer, il vaut mieux retourner une structure détaillée plutôt qu’un simple false. Cela permet de savoir si l’erreur vient de TLS, du DNS, du timeout, d’un code HTTP ou du JSON.
<?php
declare(strict_types=1);
function sky_http_get_with_curl( string $url, ?string $ca_file = null ): array {
$result = array(
'ok' => false,
'body' => '',
'http_code' => 0,
'error' => '',
'errno' => 0,
);
if ( '' === trim( $url ) || false === filter_var( $url, FILTER_VALIDATE_URL ) ) {
$result['error'] = 'Invalid URL.';
return $result;
}
$ch = curl_init( $url );
if ( false === $ch ) {
$result['error'] = 'Unable to initialise cURL.';
return $result;
}
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_USERAGENT => 'SkyMindsBot/1.0 (+https://www.skyminds.net/)',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
);
if ( null !== $ca_file && is_readable( $ca_file ) ) {
$options[ CURLOPT_CAINFO ] = $ca_file;
}
curl_setopt_array( $ch, $options );
$body = curl_exec( $ch );
$result['errno'] = curl_errno( $ch );
$result['error'] = curl_error( $ch );
$result['http_code'] = (int) curl_getinfo( $ch, CURLINFO_RESPONSE_CODE );
curl_close( $ch );
if ( false === $body ) {
return $result;
}
$result['body'] = $body;
$result['ok'] = 200 <= $result['http_code'] && 300 > $result['http_code'];
return $result;
}Code language: HTML, XML (xml)
Exemple :
<?php
$result = sky_http_get_with_curl( 'https://example.com/api.json' );
if ( ! $result['ok'] ) {
error_log(
sprintf(
'HTTP request failed. Code: %d. cURL errno: %d. Error: %s',
$result['http_code'],
$result['errno'],
$result['error']
)
);
return;
}
$data = json_decode( $result['body'], true );Code language: HTML, XML (xml)
C’est plus verbeux, mais nettement plus utile en production. Un false tout seul, c’est une porte fermée sans poignée.
Méthode 7 : utiliser un contexte SSL avec file_get_contents()
Si vous voulez garder file_get_contents(), vous pouvez lui passer un contexte SSL avec stream_context_create().
Les options SSL des streams PHP permettent notamment de définir cafile, verify_peer et verify_peer_name. Documentation PHP : options de contexte SSL
<?php
$context = stream_context_create(
array(
'ssl' => array(
'cafile' => '/etc/ssl/certs/ca-certificates.crt',
'verify_peer' => true,
'verify_peer_name' => true,
),
'http' => array(
'timeout' => 15,
'header' => "User-Agent: SkyMindsBot/1.0\r\n",
),
)
);
$response = file_get_contents( 'https://example.com/api.json', false, $context );
if ( false === $response ) {
return;
}Code language: HTML, XML (xml)
Cette méthode garde la vérification SSL active, tout en indiquant explicitement le bundle CA à utiliser.
Le faux bon conseil : désactiver la vérification SSL
On trouve souvent cette “solution” sur Internet :
<?php
$context = stream_context_create(
array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
)
);
$response = file_get_contents( 'https://example.com/api.json', false, $context );Code language: HTML, XML (xml)
Techniquement, cela peut faire disparaître l’erreur. Mais ce n’est pas une vraie correction. Cela revient à dire à PHP : “ne vérifie pas que le serveur distant est bien celui qu’il prétend être”.
À réserver uniquement à un test local très ponctuel, jamais à du code de production. La bonne solution consiste à corriger le bundle CA, pas à fermer les yeux.
Cas WordPress : utiliser wp_remote_get()
Dans WordPress, il vaut mieux éviter file_get_contents() pour appeler une API distante. Utilisez plutôt l’API HTTP de WordPress avec wp_remote_get().
La fonction wp_remote_get() effectue une requête HTTP GET et retourne soit un tableau de réponse, soit un WP_Error. La documentation WordPress précise aussi que si l’URL est contrôlée par l’utilisateur, il faut utiliser wp_safe_remote_get(). Documentation WordPress : wp_remote_get()
<?php
function sky_fetch_remote_json( string $url ): ?array {
$response = wp_remote_get(
$url,
array(
'timeout' => 15,
'redirection' => 3,
'headers' => array(
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $response ) ) {
error_log( 'Remote request failed: ' . $response->get_error_message() );
return null;
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 > $status_code || 300 <= $status_code ) {
error_log( 'Remote request returned HTTP ' . $status_code );
return null;
}
$body = wp_remote_retrieve_body( $response );
if ( '' === $body ) {
return null;
}
$data = json_decode( $body, true );
return is_array( $data ) ? $data : null;
}Code language: HTML, XML (xml)
Dans un plugin ou un thème WordPress, c’est plus propre : WordPress gère la couche HTTP, les transports disponibles, les erreurs, les timeouts, et l’intégration avec l’environnement.
Cas WordPress : wp_safe_remote_get()
Si l’URL peut venir d’un utilisateur, d’un champ d’option, d’un import ou d’un webhook, utilisez wp_safe_remote_get(). Cette fonction applique des contrôles supplémentaires pour limiter certains abus, notamment autour des URLs potentiellement dangereuses.
<?php
$response = wp_safe_remote_get(
esc_url_raw( $url ),
array(
'timeout' => 15,
'redirection' => 3,
)
);Code language: HTML, XML (xml)
Pour une URL d’API codée en dur et maîtrisée, wp_remote_get() suffit. Pour une URL variable, wp_safe_remote_get() est préférable.
Tester le certificat distant avec openssl
Avant d’accuser PHP, vérifiez aussi le certificat du serveur distant.
openssl s_client -connect example.com:443 -servername example.com -showcertsCode language: CSS (css)
Pour un diagnostic plus direct avec cURL en ligne de commande :
curl -Iv https:Code language: JavaScript (javascript)
Si curl échoue aussi côté système, le problème ne vient probablement pas de PHP seul. Il peut venir du bundle CA système, du certificat distant, ou d’un proxy réseau.
Cas Docker et environnements locaux
Dans Docker, l’erreur apparaît souvent parce que l’image est minimale et ne contient pas les certificats racines.
Pour une image Debian ou Ubuntu :
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& update-ca-certificates \
&& rm -rf /var/lib/apt/lists/*Code language: JavaScript (javascript)
Pour Alpine :
RUN apk add --no-cache ca-certificates \
&& update-ca-certificates
Ensuite, vérifiez que PHP connaît le bon chemin :
php -i | grep -E "openssl.cafile|curl.cainfo"Code language: JavaScript (javascript)
Dans Local, MAMP, XAMPP ou un environnement PHP installé à la main, le principe reste le même : trouver le php.ini réellement utilisé, puis déclarer un bundle CA lisible.
Mémo rapide
php --ini
php -i | grep -E "openssl.cafile|openssl.capath|curl.cainfo"
sudo apt update
sudo apt install ca-certificates
sudo update-ca-certificates
openssl.cafile = /etc/ssl/certs/ca-certificates.crt
curl.cainfo = /etc/ssl/certs/ca-certificates.crt
sudo systemctl restart php8.3-fpm
curl -Iv https:
openssl s_client -connect example.com:443 -servername example.com -showcertsCode language: PHP (php)
Conclusion
L’erreur file_get_contents(): SSL operation failed with code 1 indique souvent que PHP ne parvient pas à vérifier le certificat TLS du serveur distant. Dans la majorité des cas, le problème vient d’un bundle CA absent, obsolète ou mal déclaré.
La première correction consiste donc à installer les certificats racines du système, puis à configurer openssl.cafile et, si besoin, curl.cainfo dans le bon php.ini.
Si vous développez dans WordPress, utilisez plutôt wp_remote_get() ou wp_safe_remote_get(). Si vous écrivez une application PHP classique, cURL offre plus de contrôle que file_get_contents(), notamment pour les timeouts, les erreurs et la configuration SSL.
Enfin, évitez de désactiver la vérification SSL pour “corriger” le problème. Cela masque l’erreur, mais cela retire précisément la protection que TLS est censé apporter. Mieux vaut réparer le certificat que casser le thermomètre.