fix(proxy): keep the original traceback when a reloaded worker fails to import the app

This commit is contained in:
mateo-berri 2026-10-13 14:07:13 -07:00
parent c43fff79ab
commit 0a2b8da1bf
2 changed files with 33 additions and 0 deletions

View file

@ -1,3 +1,4 @@
import importlib
import logging
import sys
from collections.abc import Callable, Sequence
@ -48,7 +49,9 @@ def multi_worker_reload_supported() -> bool:
def load_app_or_exit(import_path: str) -> Callable[..., object]:
module_name: Final = import_path.partition(":")[0]
try:
importlib.import_module(module_name)
return ASGI_APP.validate_python(import_path)
except Exception:
LOGGER.exception("Error loading ASGI app %s", import_path)

View file

@ -1,5 +1,7 @@
import contextlib
import importlib
import logging
import logging.handlers
import os
import re
import signal
@ -7,6 +9,7 @@ import socket
import subprocess
import sys
import time
import traceback
from collections.abc import Callable
from pathlib import Path
from typing import Final
@ -84,6 +87,33 @@ def test_a_broken_app_exits_the_worker_as_a_startup_failure(
assert exit_info.value.code == uvicorn_config.STARTUP_FAILURE
@pytest.mark.parametrize(
("module", "source"),
(
("reload_import_error_app", "import os\nfrom os import missing_name\n"),
("reload_value_error_app", "import os\nraise ValueError('broken edit')\n"),
),
)
def test_a_broken_import_logs_the_line_that_raised(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, module: str, source: str
) -> None:
app_file: Final = tmp_path / f"{module}.py"
app_file.write_text(source)
monkeypatch.syspath_prepend(str(tmp_path))
uvicorn_logger: Final = logging.getLogger("uvicorn.error")
captured: Final = logging.handlers.BufferingHandler(capacity=POOL_WORKERS)
uvicorn_logger.addHandler(captured)
try:
with pytest.raises(SystemExit):
load_app_or_exit(f"{module}:app")
finally:
uvicorn_logger.removeHandler(captured)
(record,) = captured.buffer
assert record.exc_info is not None
raised_at: Final = frozenset((frame.filename, frame.lineno) for frame in traceback.extract_tb(record.exc_info[2]))
assert (str(app_file), 2) in raised_at, record.exc_text
def test_a_working_app_loads_as_the_module_defines_it(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
(tmp_path / "reload_working_app.py").write_text("async def app(scope, receive, send):\n return None\n")
monkeypatch.syspath_prepend(str(tmp_path))