fastapi/fastapi · critical · RuntimeError
Frontend fallback file '{fallback}' does not exist in direct
Error message
Frontend fallback file '{fallback}' does not exist in directory '{self.directory}'. Resolved absolute directory: '{self._get_resolved_directory()}' What it means
FastAPI's frontend-serving route (e.g. app.mount or the SPA-style static route) was configured with check_dir=True and an explicit fallback of 'index.html' or '404.html', but that file does not exist as a regular file inside the configured directory. The check runs eagerly in _FrontendStaticFiles.__init__ via _check_fallback_file (fastapi/routing.py:1920-1929), so the app fails at construction time, not at request time. The message reports both the configured directory and its resolved absolute path to help spot wrong working directories or missing build output.
Source
Thrown at fastapi/routing.py:1925
self.fallback = fallback
if check_dir and not os.path.isdir(directory):
raise RuntimeError(
f"Frontend directory '{directory}' does not exist. "
f"Resolved absolute path: '{_get_resolved_absolute_path(directory)}'"
)
super().__init__(
directory=directory,
html=True,
check_dir=check_dir,
follow_symlink=False,
)
if check_dir and fallback in {"index.html", "404.html"}:
self._check_fallback_file(fallback)
def _check_fallback_file(self, fallback: str) -> None:
_, stat_result = self.lookup_path(fallback)
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}'. 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)
return os.path.normpath(os.path.join(*path.split("/")))
async def get_response_for_scope(self, scope: Scope) -> Response:
if not self.config_checked:
await self.check_config()
self.config_checked = TrueView on GitHub (pinned to a1fa70d423)
Solutions
- Build the frontend so the fallback file exists in the configured directory (e.g. run 'npm run build' and confirm dist/index.html is present).
- Verify the resolved absolute directory printed in the error message; fix the 'directory' argument to point at the real build output (use an absolute path built from __file__ or Path(__file__).parent to avoid CWD issues).
- If the file is named differently (e.g. 404.htm), rename it to exactly 'index.html' or '404.html', because only those two literals are checked/served as fallbacks.
- If you intentionally want a lazy failure at request time instead of startup, pass fallback=None or 'auto' (auto resolves the file when serving) or set check_dir=False to skip eager validation.
Example fix
# before
app = FastAPI()
app.mount("/", _FrontendRoute(directory="frontend/dist", fallback="index.html", check_dir=True)) # dist not built yet
# after
import subprocess
subprocess.run(["npm", "run", "build"], cwd="frontend", check=True) # ensure dist/index.html exists
app.mount("/", _FrontendRoute(directory=str(Path(__file__).parent / "frontend" / "dist"), fallback="index.html", check_dir=True)) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def validate_frontend_dir(directory: str | Path, fallback: str | None) -> None:
d = Path(directory).resolve()
assert d.is_dir(), f"directory missing: {d}"
if fallback in {"index.html", "404.html"}:
f = d / fallback
assert f.is_file(), f"fallback file missing: {f}" Type guard
def is_valid_fallback(value: str | None) -> bool:
return value in {"auto", "index.html", "404.html", None} Try / catch
try:
route = _FrontendRoute("/", directory=dist, fallback="index.html", check_dir=True)
except RuntimeError as e:
raise SystemExit(f"Frontend build missing, run 'npm run build' first: {e}") from e Prevention
- Build the frontend in CI before starting the API, and fail the pipeline if dist/index.html is absent.
- Use absolute paths derived from __file__ for the static directory instead of CWD-relative strings.
- Keep check_dir=True in production so a broken frontend deploy fails at startup, not per-request.
When it happens
Trigger: Calling the frontend route helper with directory='./frontend/dist', fallback='index.html' (or '404.html'), check_dir=True, when 'frontend/dist/index.html' is absent (frontend not built, file named differently, or directory resolved relative to a different CWD). Only fires for fallback values 'index.html' and '404.html'; 'auto' and None skip the check (routing.py:1919).
Common situations: Serving an SPA where the JS build step (npm run build / vite build) was never run or output went to a different folder; CI pipelines that start the API before building the frontend; Docker images that copy the wrong dist directory; relative directory paths resolved against an unexpected working directory.
Related errors
- A frontend path cannot be empty
- fallback must be 'auto', 'index.html', '404.html', or None
- A frontend path must start with '/'
- Prefix and path cannot be both empty (path operation: {name}
- No PR number available
AI-assisted analysis of fastapi/fastapi@a1fa70d423 (2026-08-14).
Data as JSON: /api/errors/765a7fe6af914667.
Report an issue: GitHub.