This commit is contained in:
2026-02-11 18:16:07 -08:00
parent cc715e1fef
commit 1d0de1118b
27 changed files with 1167 additions and 1075 deletions

View File

@@ -32,122 +32,8 @@ except Exception: # pragma: no cover - optional dependency
logger = logging.getLogger(__name__)
def _resolve_verify_value(verify_ssl: bool) -> Union[bool, str]:
"""Return the httpx verify argument, preferring system-aware bundles.
Order of precedence:
1. If verify_ssl is not True (False or path), return it.
2. Respect existing SSL_CERT_FILE env var if present.
3. Prefer `pip_system_certs` if present and it exposes a bundle path.
4. Prefer `certifi_win32`/similar helpers by invoking them and reading certifi.where().
5. Fall back to `certifi.where()` if available.
6. Otherwise, return True to let httpx use system defaults.
"""
if verify_ssl is not True:
return verify_ssl
env_cert = os.environ.get("SSL_CERT_FILE")
if env_cert:
return env_cert
def _try_module_bundle(mod_name: str) -> Optional[str]:
# Prefer checking sys.modules first (helps test injection / monkeypatching)
mod = sys.modules.get(mod_name)
if mod is None:
# Avoid raising ModuleNotFoundError so debuggers and callers aren't interrupted.
# Check for module availability before attempting to import it.
try:
import importlib.util
spec = importlib.util.find_spec(mod_name)
if spec is None:
return None
import importlib
mod = importlib.import_module(mod_name)
except Exception:
# Treat any import/initialization failure as module not available.
return None
# Common APIs that return a bundle path
for attr in ("where", "get_ca_bundle", "bundle_path", "get_bundle_path", "get_bundle"):
fn = getattr(mod, attr, None)
if callable(fn):
try:
res = fn()
if res:
return res
except Exception:
continue
elif isinstance(fn, str) and fn:
return fn
# Some helpers (e.g., certifi_win32) expose an action to merge system certs
for call_attr in ("add_windows_store_certs", "add_system_certs", "merge_system_certs"):
fn = getattr(mod, call_attr, None)
if callable(fn):
try:
fn()
try:
import certifi as _certifi
res = _certifi.where()
if res:
return res
except Exception:
logger.exception("Failed while probing certifi helper inner block")
except Exception:
logger.exception("Failed while invoking cert helper function")
return None
# Prefer helpful modules if available (use safe checks to avoid first-chance import errors)
for mod_name in ("pip_system_certs", "certifi_win32"):
path = _try_module_bundle(mod_name)
if path:
try:
os.environ["SSL_CERT_FILE"] = path
except Exception:
logger.exception("Failed to set SSL_CERT_FILE environment variable")
logger.info(f"SSL_CERT_FILE not set; using bundle from {mod_name}: {path}")
return path
# Fallback to certifi
try:
import certifi # type: ignore
path = certifi.where()
if path:
try:
os.environ["SSL_CERT_FILE"] = path
except Exception:
logger.exception("Failed to set SSL_CERT_FILE environment variable during certifi fallback")
logger.info(f"SSL_CERT_FILE not set; using certifi bundle: {path}")
return path
except Exception:
logger.exception("Failed to probe certifi for trust bundle")
# Fallback to certifi
try:
import certifi # type: ignore
path = certifi.where()
if path:
try:
os.environ["SSL_CERT_FILE"] = path
except Exception:
logger.exception("Failed to set SSL_CERT_FILE environment variable during certifi fallback")
logger.info(f"SSL_CERT_FILE not set; using certifi bundle: {path}")
return path
except Exception:
logger.exception("Failed to probe certifi for trust bundle")
return True
def get_requests_verify_value(verify_ssl: bool = True) -> Union[bool, str]:
"""Expose the verified value for reuse outside of HTTPClient (requests sessions)."""
return _resolve_verify_value(verify_ssl)
from API.ssl_certs import resolve_verify_value as _resolve_verify_value
from API.ssl_certs import get_requests_verify_value
# Default configuration
DEFAULT_TIMEOUT = 30.0
@@ -444,13 +330,16 @@ class HTTPClient:
"HTTPClient must be used with context manager (with statement)"
)
# Merge headers
if "headers" in kwargs and kwargs["headers"]:
headers = self._get_headers()
headers.update(kwargs["headers"])
kwargs["headers"] = headers
else:
kwargs["headers"] = self._get_headers()
# Merge headers once per call (do not rebuild for every retry attempt).
merged_headers = self._get_headers()
extra_headers = kwargs.get("headers")
if extra_headers:
try:
merged_headers.update(extra_headers)
except Exception:
# If headers is not a mapping, keep it as-is and let httpx raise.
merged_headers = extra_headers
kwargs["headers"] = merged_headers
last_exception: Exception | None = None