From c82dc3530d67fe68c659b562e692df24fbfc373e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Jun 2026 02:09:49 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8A=20Make=20path=20errors=20show=20ab?= =?UTF-8?q?solute=20path=20to=20help=20with=20debugging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 19 +++++++++++++++++-- tests/test_frontend.py | 30 ++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 87a458967..bd6289b58 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1794,6 +1794,10 @@ def _frontend_path_specificity(path: str) -> int: return len(path) +def _get_resolved_absolute_path(path: str | os.PathLike[str]) -> str: + return os.path.realpath(os.fspath(path)) + + class _FrontendStaticFiles(StaticFiles): def __init__( self, @@ -1803,6 +1807,11 @@ class _FrontendStaticFiles(StaticFiles): check_dir: bool = True, ) -> None: self.fallback = fallback + if check_dir and not os.path.isdir(directory): + raise RuntimeError( + f"Frontend directory {directory!r} does not exist. " + f"Resolved absolute path: {_get_resolved_absolute_path(directory)!r}" + ) super().__init__( directory=directory, html=True, @@ -1817,9 +1826,14 @@ class _FrontendStaticFiles(StaticFiles): if stat_result is None or not stat.S_ISREG(stat_result.st_mode): raise RuntimeError( f"Frontend fallback file '{fallback}' does not exist in " - f"directory '{self.directory}'" + f"directory '{self.directory}'. Resolved absolute directory: " + f"'{self._get_resolved_directory()}'" ) + def _get_resolved_directory(self) -> str: + assert self.directory is not None + return _get_resolved_absolute_path(self.directory) + def get_path(self, scope: Scope) -> str: path = _get_fastapi_scope(scope).get(_FASTAPI_FRONTEND_PATH_KEY, "") assert isinstance(path, str) @@ -1879,7 +1893,8 @@ class _FrontendStaticFiles(StaticFiles): if stat_result is None or not stat.S_ISREG(stat_result.st_mode): raise RuntimeError( f"Frontend fallback file '{fallback}' does not exist in " - f"directory '{self.directory}'" + f"directory '{self.directory}'. Resolved absolute directory: " + f"'{self._get_resolved_directory()}'" ) return self.file_response( full_path, stat_result, scope, status_code=status_code diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 0adf911ec..12be8eaf2 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -692,11 +692,16 @@ def test_symlink_outside_directory_is_not_served(tmp_path: Path): assert response.text != "secret" -def test_check_dir_true_fails_early_for_missing_directory(tmp_path: Path): +def test_check_dir_true_fails_early_for_missing_directory(monkeypatch, tmp_path: Path): app = FastAPI() + monkeypatch.chdir(tmp_path) - with pytest.raises(RuntimeError, match="does not exist"): - app.frontend("/", directory=tmp_path / "missing") + with pytest.raises(RuntimeError, match="does not exist") as exc_info: + app.frontend("/", directory="missing") + + message = str(exc_info.value) + assert "'missing'" in message + assert str(tmp_path / "missing") in message def test_check_dir_false_allows_missing_directory_and_fails_on_request(tmp_path: Path): @@ -707,20 +712,29 @@ def test_check_dir_false_allows_missing_directory_and_fails_on_request(tmp_path: TestClient(app).get("/asset.txt") -def test_explicit_fallback_files_fail_clearly_when_missing(tmp_path: Path): +def test_explicit_fallback_files_fail_clearly_when_missing(monkeypatch, tmp_path: Path): dist = tmp_path / "dist" dist.mkdir() + monkeypatch.chdir(tmp_path) app = FastAPI() - with pytest.raises(RuntimeError, match="index.html"): - app.frontend("/", directory=dist, fallback="index.html") + with pytest.raises(RuntimeError, match="index.html") as exc_info: + app.frontend("/", directory="dist", fallback="index.html") + + message = str(exc_info.value) + assert "directory 'dist'" in message + assert str(dist) in message app = FastAPI() - app.frontend("/", directory=dist, fallback="404.html", check_dir=False) + app.frontend("/", directory="dist", fallback="404.html", check_dir=False) - with pytest.raises(RuntimeError, match="404.html"): + with pytest.raises(RuntimeError, match="404.html") as exc_info: TestClient(app).get("/missing.js") + message = str(exc_info.value) + assert "directory 'dist'" in message + assert str(dist) in message + def test_frontend_routes_are_not_in_openapi(tmp_path: Path): dist = tmp_path / "dist"