diff --git a/hermes_cli/urllib_security.py b/hermes_cli/urllib_security.py index 85f311885b..d8626bd13c 100644 --- a/hermes_cli/urllib_security.py +++ b/hermes_cli/urllib_security.py @@ -167,9 +167,9 @@ def _build_https_context(candidates: tuple[str, ...]) -> tuple[ssl.SSLContext | try: return ssl.create_default_context(cafile=path), path except (OSError, ssl.SSLError) as exc: - logger.warning( - "CA bundle could not be loaded from %s: %s — falling back to default certificates", path, exc - ) + logger.warning("CA bundle could not be loaded from %s: %s — trying the next bundle", path, exc) + if candidates: + logger.warning("No configured CA bundle could be loaded — falling back to default certificates") return None, None diff --git a/tests/hermes_cli/test_urllib_security.py b/tests/hermes_cli/test_urllib_security.py index 3e901402f2..c7bb2f5c98 100644 --- a/tests/hermes_cli/test_urllib_security.py +++ b/tests/hermes_cli/test_urllib_security.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import logging from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import ssl from threading import Thread @@ -600,6 +601,30 @@ def test_fallback_bundle_change_does_not_invalidate_the_memo(monkeypatch, tmp_pa assert calls == [str(preferred)] +def test_default_certificates_fallback_is_logged_once_after_all_bundles_fail(monkeypatch, caplog): + """A failed candidate says "trying the next bundle"; the default-certificates line is emitted once.""" + import hermes_cli.urllib_security as urllib_security + + def create_default_context(*, cafile=None): + raise ssl.SSLError(f"bad bundle {cafile}") + + monkeypatch.setattr(ssl, "create_default_context", create_default_context) + + with caplog.at_level(logging.WARNING, logger=urllib_security.logger.name): + assert urllib_security._build_https_context(("/a.pem", "/b.pem")) == (None, None) + + messages = [record.getMessage() for record in caplog.records] + per_failure = [m for m in messages if "trying the next bundle" in m] + assert [m.split(":")[0] for m in per_failure] == [ + "CA bundle could not be loaded from /a.pem", + "CA bundle could not be loaded from /b.pem", + ] + assert [m for m in messages if "falling back to default certificates" in m] == [ + "No configured CA bundle could be loaded — falling back to default certificates" + ] + assert all(record.levelno == logging.WARNING for record in caplog.records) + + @pytest.mark.parametrize( ("candidates", "first_load_fails_for", "expected_load_sequence"), [