From a10f511a13b871273832ffc19cd0c6d2e6f4e809 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 21 Jun 2026 00:42:58 +0000 Subject: [PATCH] fix(#4561): catch RecursionError on deeply-nested manifest (BRICK) Codex+Opus ship-gate: a <=64KB but deeply-nested JSON manifest makes json.loads raise RecursionError, which escaped _read_manifest_urls into the app-shell route -> every page load 503s. Add RecursionError to the fail-safe except + regression test (verified red-without/green-with). Co-authored-by: santastabber --- api/extensions.py | 6 ++++++ tests/test_extension_hooks.py | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/api/extensions.py b/api/extensions.py index 179329578..3ea0182d3 100644 --- a/api/extensions.py +++ b/api/extensions.py @@ -253,6 +253,12 @@ def _read_manifest_urls(root: Path) -> Tuple[List[str], List[str]]: except json.JSONDecodeError: _log.warning("Configured extension manifest is not valid JSON") return [], [] + except RecursionError: + # A <=64KB but deeply-nested manifest makes json.loads exceed the + # interpreter recursion limit. Without this, the RecursionError escapes + # into the app-shell route and every page load 503s. Fail safe. + _log.warning("Configured extension manifest is too deeply nested") + return [], [] except (OSError, UnicodeDecodeError): _log.warning("Configured extension manifest could not be read") return [], [] diff --git a/tests/test_extension_hooks.py b/tests/test_extension_hooks.py index d78638f23..e3daed633 100644 --- a/tests/test_extension_hooks.py +++ b/tests/test_extension_hooks.py @@ -228,6 +228,29 @@ def test_extension_manifest_reuses_url_safety_rules(tmp_path, monkeypatch): } +def test_extension_manifest_deeply_nested_json_fails_safe(tmp_path, monkeypatch): + """A <=64KB but deeply-nested manifest makes json.loads raise RecursionError. + It must fail safe (empty lists) — NOT escape and 503 every page load.""" + monkeypatch.delenv("HERMES_WEBUI_EXTENSION_SCRIPT_URLS", raising=False) + monkeypatch.delenv("HERMES_WEBUI_EXTENSION_STYLESHEET_URLS", raising=False) + root = tmp_path / "extensions" + root.mkdir() + # ~6000 nested arrays: well under 64KB but blows the recursion limit in json.loads. + depth = 6000 + payload = "[" * depth + "]" * depth + assert len(payload.encode("utf-8")) < 64 * 1024 + (root / "deep.json").write_text(payload, encoding="utf-8") + monkeypatch.setenv("HERMES_WEBUI_EXTENSION_DIR", str(root)) + monkeypatch.setenv("HERMES_WEBUI_EXTENSION_MANIFEST", "deep.json") + + from api.extensions import get_extension_config + + # Must not raise; must fall back to no manifest assets. + cfg = get_extension_config() + assert cfg["script_urls"] == [] + assert cfg["stylesheet_urls"] == [] + + def test_extension_manifest_path_must_stay_inside_extension_root(tmp_path, monkeypatch): monkeypatch.delenv("HERMES_WEBUI_EXTENSION_STYLESHEET_URLS", raising=False) root = tmp_path / "extensions"