headroom/tests/test_proxy_extensions.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

47 lines
1.4 KiB
Python
Raw Permalink Normal View History

fix(proxy): one bad extension no longer aborts proxy startup (#2215) ## What `install_all()` (the `headroom.proxy_extension` loader) previously let any exception from an extension's `install()` **propagate and abort proxy startup** — one broken or version-incompatible third-party extension took the whole proxy down, and every other extension with it. This makes extension loading resilient: - catch a failing `install()`, log it (with traceback), record it as **skipped** - continue installing the rest — a failure disables that one extension, not the proxy - print a `SKIPPED` line to the console (the startup banner lists *enabled* extensions before install runs, so a skip would otherwise be logging-config dependent) ## Why Found while testing several proxy extensions together in a clean venv: a plugin built against a newer core API raised `ModuleNotFoundError` from `install()` and crashed the proxy at startup. An extension that fails its own environment/auth check should disable itself — it should not take the whole proxy down. ## Real behavior proof Before — one extension failing in `install()`: ``` ... proxy did NOT come up (/livez never answered) ``` After — same setup, one extension deliberately broken: ``` [headroom] proxy extensions SKIPPED: myorg_ext (install failed — running without them; see logs) /livez: 200 healthy # proxy up; the other extensions installed ``` Loader unit check (fake failing extension): ``` returned installed: ['good_ext'] # bad one excluded bad_ext skipped (not in installed): True good_ext survived: True warning logged for bad_ext: True ``` ## Tests - `mypy headroom/proxy/extensions.py` → `Success: no issues found` - `ruff check headroom/proxy/extensions.py` → `All checks passed!` - Verified in-process (catch/skip/continue + logging) and end-to-end against a running proxy (`/livez` 200 with a deliberately failing extension). ## Maintainer Follow-up - Added `tests/test_proxy_extensions.py` covering skip-and-continue behavior for a failed extension and the missing-extension warning path. - Removed an informal implementation comment from `headroom/proxy/extensions.py`. - Validation on `fe5176db`: `uv run --frozen --extra dev python -m pytest tests/test_proxy_extensions.py -q`, `uvx ruff==0.15.17 check headroom/proxy/extensions.py tests/test_proxy_extensions.py --output-format concise`, `git diff --check`, and commit hooks all passed. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 22:50:03 -07:00
from __future__ import annotations
import logging
from typing import Any
from headroom.proxy import extensions
def test_install_all_skips_failed_extension_and_continues(
caplog,
capsys,
monkeypatch,
) -> None:
calls: list[str] = []
def good(app: Any, config: Any) -> None:
calls.append("good")
def bad(app: Any, config: Any) -> None:
calls.append("bad")
raise RuntimeError("missing optional dependency")
monkeypatch.setattr(
extensions,
"discover",
lambda: iter([("bad_ext", bad), ("good_ext", good)]),
)
with caplog.at_level(logging.WARNING, logger=extensions.log.name):
installed = extensions.install_all(object(), object(), enabled=["bad_ext", "good_ext"])
assert installed == ["good_ext"]
assert calls == ["bad", "good"]
assert "bad_ext" in capsys.readouterr().err
assert "failed to install and was skipped" in caplog.text
assert "proxy extensions skipped due to install errors: bad_ext" in caplog.text
def test_install_all_warns_for_missing_requested_extension(caplog, monkeypatch) -> None:
monkeypatch.setattr(extensions, "discover", lambda: iter([]))
with caplog.at_level(logging.WARNING, logger=extensions.log.name):
installed = extensions.install_all(object(), object(), enabled=["missing_ext"])
assert installed == []
assert "proxy extensions requested but not found: missing_ext" in caplog.text