Un script Python qui fonctionne partout échoue sur le poste du bureau avec CERTIFICATE_VERIFY_FAILED. La réponse n’est pas de couper la vérification, mais de faire confiance à la bonne autorité de certification.
Le symptôme
Observé sur un poste Windows d’entreprise dont le proxy inspecte le TLS : httpx, requests et huggingface_hub échouent tous sur la même ligne.
[SSL: CERTIFICATE_VERIFY_FAILED] unable to get local issuer certificate
Le proxy re-signe le trafic avec une autorité de certification (CA) d’entreprise. Elle figure dans le magasin de certificats Windows, pas dans le bundle certifi que requests et httpx utilisent par défaut, selon leurs documentations.
Piège observé : pip install passait, l’index de paquets n’étant pas intercepté. Un pip qui réussit ne prouve rien pour les autres appels HTTPS.
truststore : utiliser le magasin du système
truststore expose les magasins natifs (CryptoAPI sous Windows, Security framework sous macOS, OpenSSL sous Linux) via une API calquée sur ssl.SSLContext. Il demande Python 3.10 ou plus.
import truststore
truststore.inject_into_ssl() # en tout premier
import httpx # imports réseau ensuite
r = httpx.get("https://huggingface.co/api/models?limit=1")
r.raise_for_status()
inject_into_ssl() patche le module ssl : requests, urllib3 et aiohttp en profitent sans autre changement, et nous l’avons observé aussi avec httpx. La documentation pose deux règles : l’appeler le plus tôt possible, car un module qui a déjà importé ssl.SSLContext n’est pas affecté ; et le réserver aux applications et aux scripts, jamais à une bibliothèque.
Pour éviter le patch global, on passe un contexte à un seul client, comme dans l’exemple de la documentation httpx :
import ssl
import truststore
import httpx
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client = httpx.Client(verify=ctx)
La bibliothèque standard est moins exposée : sous Windows, ssl.create_default_context() charge déjà les magasins CA et ROOT du système. Ce sont les bibliothèques qui imposent certifi qui cassent.
Sans truststore : les variables de bundle
Si truststore n’est pas envisageable, exportez la chaîne de la CA d’entreprise au format PEM dans un fichier, puis désignez-le :
$env:REQUESTS_CA_BUNDLE = "C:\certs\bundle.pem" # requests
$env:SSL_CERT_FILE = "C:\certs\bundle.pem" # httpx
REQUESTS_CA_BUNDLE, avec repli surCURL_CA_BUNDLE, remplace le bundle par défaut de requests. Le fichier doit donc contenir toutes les autorités utiles, pas seulement celle du proxy.- requests ignore l’environnement dans le flux des requêtes préparées : la variable n’est pas lue et l’erreur revient, sauf à appeler
Session.merge_environment_settings(). - httpx respecte
SSL_CERT_FILEetSSL_CERT_DIRpar défaut.
Le cas Hugging Face
Depuis la v1.0, huggingface_hub utilise httpx à la place de requests. C’est donc SSL_CERT_FILE qui compte. Autre conséquence documentée : l’erreur SSL n’existe plus en tant que telle, elle devient un httpx.ConnectError générique. Lisez le message, pas le type d’exception.
Sur le même poste, un second ConnectError attendait :
ConnectError: [Errno 11001] getaddrinfo failed
Avec hf-xet présent, les gros fichiers passaient par un hôte du CDN Xet, cas-bridge.xethub.hf.co, que le DNS interne ne résolvait pas. huggingface.co répondait en 200 : l’API semblait joignable, seul le téléchargement cassait. Contournement retenu, le retour au transport HTTP classique :
import os
os.environ.setdefault("HF_HUB_DISABLE_XET", "1") # avant l'import
import truststore
truststore.inject_into_ssl()
from huggingface_hub import hf_hub_download
L’ordre compte : la documentation Hugging Face précise que ses variables sont lues à l’import de huggingface_hub, toute modification ultérieure étant ignorée. Elle invite aussi, quand on désactive Xet, à transmettre un diagnostic à l’équipe du projet.
Ce qu’il faut retenir
CERTIFICATE_VERIFY_FAILEDderrière un proxy d’entreprise signale une CA absente decertifi, pas une raison de couper la vérification.truststore.inject_into_ssl()en tête de script, outruststore.SSLContextpassé à un client précis.- Sans truststore :
REQUESTS_CA_BUNDLEpour requests,SSL_CERT_FILEpour httpx, avec un bundle complet. huggingface_hubv1 passe par httpx : une erreur SSL y prend la forme d’unConnectError.getaddrinfo failedsur un gros fichier alors que l’API répond :HF_HUB_DISABLE_XET=1, posé avant l’import.
In English
Behind a TLS-inspecting proxy, requests, httpx and huggingface_hub fail certificate verification. truststore, bundle variables and the Xet CDN pitfall, without ever turning verification off. truststore makes libraries use the system store and must be injected before network imports, and huggingface_hub v1 goes through httpx, so it needs SSL_CERT_FILE rather than REQUESTS_CA_BUNDLE.
Sources & vérification
Testé sur : Poste Windows derrière un proxy à inspection TLS, huggingface_hub avec hf-xet
- truststore, documentation truststore.readthedocs.io
- Requests, Advanced Usage (SSL, CA Certificates, Proxies) requests.readthedocs.io
- HTTPX, SSL python-httpx.org
- Python, module ssl docs.python.org
- huggingface_hub, variables d'environnement huggingface.co
- huggingface_hub, migration vers la v1.0 huggingface.co
- Expérience directe de l’auteur, en production (versions ci-dessus)
