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 = True

View on GitHub (pinned to a1fa70d423)

Solutions

  1. 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).
  2. 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).
  3. 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.
  4. 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

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


AI-assisted analysis of fastapi/fastapi@a1fa70d423 (2026-08-14). Data as JSON: /api/errors/765a7fe6af914667. Report an issue: GitHub.