{* ../../docs_src/frontend/tutorial002_py310.py hl[5] *}
-**FastAPI** uses this fallback only for `GET` and `HEAD` requests that look like browser navigation. Missing files like JavaScript, CSS, and images still return `404`.
+**FastAPI** uses this fallback only for `GET` and `HEAD` requests that explicitly accept HTML with `Accept: text/html` or `Accept: application/xhtml+xml`, as browser navigation requests normally do. Missing files like JavaScript, CSS, and images still return `404`.
Requests with other methods, like `POST` or `PUT`, to paths that only match the frontend fallback also return `404`. Regular **FastAPI** *path operations* still have higher priority than frontend routes.
def _is_frontend_navigation_request(scope: Scope) -> bool:
- route_path = get_route_path(scope)
- final_segment = route_path.rsplit("/", 1)[-1]
- if os.path.splitext(final_segment)[1]:
- return False
request = Request(scope)
- wildcard_accepted = False
- html_rejected = False
for media_type, quality in _iter_accept_media_types(
request.headers.get("accept", "")
):
- if media_type in {"text/html", "application/xhtml+xml"}:
- if quality == 0:
- html_rejected = True
- else:
- return True
- elif media_type == "*/*" and quality != 0:
- wildcard_accepted = True
- return wildcard_accepted and not html_rejected
+ if media_type in {"text/html", "application/xhtml+xml"} and quality != 0:
+ return True
+ return False
class _FrontendRoute(BaseRoute):
app = FastAPI()
app.frontend("/", directory=dist)
- response = TestClient(app).get("/app")
+ response = TestClient(app).get("/app", headers={"accept": "text/html"})
assert response.status_code == 200
assert response.text == "app"
assert response.json() == {"detail": "api missing"}
-def test_index_fallback_for_navigation_request(tmp_path: Path):
+@pytest.mark.parametrize(
+ ("path", "accept"),
+ [
+ ("/dashboard/settings", "text/html"),
+ ("/users/jane.doe", "text/html"),
+ ("/v/1.2.3", "application/xhtml+xml"),
+ ],
+)
+def test_index_fallback_for_navigation_request(tmp_path: Path, path: str, accept: str):
dist = tmp_path / "dist"
write_file(dist / "index.html", "app shell")
app = FastAPI()
app.frontend("/", directory=dist, fallback="index.html")
- response = TestClient(app).get(
- "/dashboard/settings", headers={"accept": "text/html"}
- )
+ response = TestClient(app).get(path, headers={"accept": accept})
assert response.status_code == 200
assert response.text == "app shell"
app.frontend("/", directory=dist, fallback="index.html")
response = TestClient(app).get(
- "/dashboard/settings", headers={"accept": "text/html; q=0.8"}
+ "/dashboard/settings",
+ headers={"accept": "text/html; charset=utf-8; q=0.8"},
)
assert response.status_code == 200
("/assets/missing.css", "text/css"),
("/assets/missing.png", "image/png"),
("/api/missing", "application/json"),
- ("/users/jane.doe", "text/html"),
+ ("/dashboard/settings", ""),
],
)
-def test_index_fallback_does_not_handle_asset_like_or_non_html_requests(
+def test_index_fallback_requires_explicit_html_acceptance(
tmp_path: Path, path: str, accept: str
):
dist = tmp_path / "dist"