From e614854e08a1a7eb8435b8e101772cfe84000bd3 Mon Sep 17 00:00:00 2001 From: Vinay Kumar Date: Tue, 11 Aug 2026 20:37:14 +0530 Subject: [PATCH] Fixes #1632: fall back to certifi when the OS trust store is empty On platforms where OpenSSL's default trust store is empty (notably python.org macOS builds, which rely on certifi instead), HTTPie could not verify any TLS certificate by default, even though `requests` in the very same environment worked fine. Because HTTPie always hands a custom SSLContext to urllib3, urllib3 skips its own `load_default_certs()` path (it only does that for contexts it created itself), and Requests likewise skips preloading its certifi-backed context. The result is a context with zero CA certificates whenever `load_default_certs()` finds nothing. `ensure_default_certs_loaded()` now falls back to the same CA bundle Requests would have used (certifi) when the platform default store turns out to be empty, so HTTPie is never less capable than requests. Explicit `--verify=` and `--verify=no` behavior is unchanged, and untrusted/self-signed certificates are still rejected. --- httpie/compat.py | 51 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/httpie/compat.py b/httpie/compat.py index d12abcff02..8f5a1879bc 100644 --- a/httpie/compat.py +++ b/httpie/compat.py @@ -1,3 +1,4 @@ +import os import sys from ssl import SSLContext from typing import Any, Optional, Iterable @@ -103,11 +104,51 @@ def get_dist_name(entry_point: importlib_metadata.EntryPoint) -> Optional[str]: def ensure_default_certs_loaded(ssl_context: SSLContext) -> None: """ - Workaround for a bug in Requests 2.32.3 + Ensure the given SSL context has a usable set of CA certificates loaded. - See + Historically this only worked around a bug in Requests 2.32.3 + () by calling + `load_default_certs()`. However, on platforms where OpenSSL's default + trust store is empty (most notably python.org macOS builds, which rely + on certifi rather than a system OpenSSL trust store), + `load_default_certs()` loads nothing at all, leaving HTTPie unable to + verify any certificate even though `requests` itself works fine. + + Because we always pass a custom SSLContext down to urllib3, urllib3 + skips its own default-cert loading, and Requests skips its certifi + preloading too — so nobody loads certifi for us. Fall back to the CA + bundle Requests would have used (certifi) in that case. + + See """ - if hasattr(ssl_context, 'load_default_certs'): - if not ssl_context.get_ca_certs(): - ssl_context.load_default_certs() + if not hasattr(ssl_context, 'load_default_certs'): + # Custom pyOpenSSL contexts don't support it. + return + + if ssl_context.get_ca_certs(): + return + + ssl_context.load_default_certs() + if ssl_context.get_ca_certs(): + return + + # The platform's default trust store is empty (e.g. python.org macOS + # builds). Fall back to the CA bundle Requests itself uses (certifi), + # so that HTTPie is never less capable than `requests` by default. + try: + from requests.utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths + except ImportError: + return + + if not DEFAULT_CA_BUNDLE_PATH: + return + + ca_bundle_path = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH) + if not ca_bundle_path or not os.path.exists(ca_bundle_path): + return + + if os.path.isdir(ca_bundle_path): + ssl_context.load_verify_locations(capath=ca_bundle_path) + else: + ssl_context.load_verify_locations(cafile=ca_bundle_path)