headroom/tests/test_proxy_extensions.py
Tejas Chopra cb6c828457
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

46 lines
1.4 KiB
Python

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