tiangolo/fastapi · error · AssertionError

fallback must be 'auto', 'index.html', '404.html', or None

Error message

fallback must be 'auto', 'index.html', '404.html', or None

What it means

`_FrontendRoute.__init__` (routing.py:2056-2059) raises `AssertionError` when `fallback` is not one of `{"auto", "index.html", "404.html", None}`. The parameter is typed `Literal[...]`, so a static type checker flags it, but at runtime any other value (e.g. `"error.html"`, `True`, `"Auto"`) triggers this assertion at route construction.

Source

Thrown at fastapi/routing.py:2057

    for media_type, quality in _iter_accept_media_types(
        request.headers.get("accept", "")
    ):
        if media_type in {"text/html", "application/xhtml+xml"} and quality != 0:
            return True
    return False


class _FrontendRoute(BaseRoute):
    def __init__(
        self,
        path: str,
        *,
        directory: str | os.PathLike[str],
        fallback: Literal["auto", "index.html", "404.html"] | None = "auto",
        check_dir: bool,
    ) -> None:
        if fallback not in {"auto", "index.html", "404.html", None}:
            raise AssertionError(
                "fallback must be 'auto', 'index.html', '404.html', or None"
            )
        self.path = _normalize_frontend_path(path)
        self.methods = {"GET", "HEAD"}
        self.app = _FrontendStaticFiles(
            directory=directory, fallback=fallback, check_dir=check_dir
        )

    def matches(self, scope: Scope) -> tuple[Match, Scope]:
        return self.matches_with_path(scope, self.path)

    def matches_with_path(self, scope: Scope, path: str) -> tuple[Match, Scope]:
        if scope["type"] != "http":
            return Match.NONE, {}
        frontend_path = self._get_frontend_path(path, get_route_path(scope))
        if frontend_path is None:
            return Match.NONE, {}
        child_scope = {

View on GitHub (pinned to 42a41db11f)

Solutions

  1. Use one of the allowed literals: `"auto"`, `"index.html"`, `"404.html"`, or `None`.
  2. If you need a custom page, name the file `404.html` (or `index.html`) in the directory and select that literal.
  3. Validate config-driven `fallback` against the allowed set before passing it to `.frontend()`.
  4. Run a type checker (mypy/pyright) — the `Literal` annotation will flag invalid values.

Example fix

# before
app.frontend("/", directory="dist", fallback="error.html")

# after
# rename your file to dist/404.html, then:
app.frontend("/", directory="dist", fallback="404.html")
Defensive patterns

Strategy: type-guard

Validate before calling

from typing import Literal

_ALLOWED = {"auto", "index.html", "404.html", None}

def coerce_fallback(value: str | None) -> Literal["auto", "index.html", "404.html"] | None:
    if value not in _ALLOWED:
        raise ValueError(
            f"fallback must be one of {sorted(v for v in _ALLOWED if v)}, got {value!r}"
        )
    return value  # type: ignore[return-value]

# usage
app.frontend("/", directory="dist", fallback=coerce_fallback(cfg.get("FALLBACK", "auto")))

Type guard

from typing import Literal

def is_allowed_fallback(value: object) -> bool:
    return value in {"auto", "index.html", "404.html", None}

# use as a type guard:
def to_fallback(value: object) -> Literal["auto", "index.html", "404.html"] | None:
    if not is_allowed_fallback(value):
        raise TypeError(f"unsupported fallback: {value!r}")
    return value  # type: ignore[return-value]

Prevention

When it happens

Trigger: Passing `app.frontend("/", directory="dist", fallback="error.html")`, `fallback="/index.html"`, `fallback=True`, or any casing variant like `"AUTO"`. Reading the value from config without validating against the allowed set.

Common situations: Wanting a custom error page name and guessing the API accepts arbitrary filenames; typos; config files supplying an unsupported string; copy-pasting an older/newer API's value.

Related errors


AI-assisted analysis of tiangolo/fastapi@42a41db11f (2026-08-04). Data as JSON: /data/errors/e3a526c6cc3a4ed1.json. Report an issue: GitHub.