Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eb75fd0

Browse filesBrowse files
authored
🐛 Fix frontend fallback support for doted paths like /users/john.doe (#16011)
1 parent 9b8410b commit eb75fd0
Copy full SHA for eb75fd0

3 files changed

+19-23Lines changed: 19 additions & 23 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎docs/en/docs/tutorial/frontend.md‎

Copy file name to clipboardExpand all lines: docs/en/docs/tutorial/frontend.md
+1-1Lines changed: 1 addition & 1 deletion
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ For that, use `fallback="index.html"`:
5252

5353
{* ../../docs_src/frontend/tutorial002_py310.py hl[5] *}
5454

55-
**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`.
55+
**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`.
5656

5757
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.
5858

Collapse file

‎fastapi/routing.py‎

Copy file name to clipboardExpand all lines: fastapi/routing.py
+3-14Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,24 +1984,13 @@ def _iter_accept_media_types(accept: str) -> Iterator[tuple[str, float]]:
19841984

19851985

19861986
def _is_frontend_navigation_request(scope: Scope) -> bool:
1987-
route_path = get_route_path(scope)
1988-
final_segment = route_path.rsplit("/", 1)[-1]
1989-
if os.path.splitext(final_segment)[1]:
1990-
return False
19911987
request = Request(scope)
1992-
wildcard_accepted = False
1993-
html_rejected = False
19941988
for media_type, quality in _iter_accept_media_types(
19951989
request.headers.get("accept", "")
19961990
):
1997-
if media_type in {"text/html", "application/xhtml+xml"}:
1998-
if quality == 0:
1999-
html_rejected = True
2000-
else:
2001-
return True
2002-
elif media_type == "*/*" and quality != 0:
2003-
wildcard_accepted = True
2004-
return wildcard_accepted and not html_rejected
1991+
if media_type in {"text/html", "application/xhtml+xml"} and quality != 0:
1992+
return True
1993+
return False
20051994

20061995

20071996
class _FrontendRoute(BaseRoute):
Collapse file

‎tests/test_frontend.py‎

Copy file name to clipboardExpand all lines: tests/test_frontend.py
+15-8Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_frontend_exact_prefix_path_serves_index(tmp_path: Path):
3232
app = FastAPI()
3333
app.frontend("/", directory=dist)
3434

35-
response = TestClient(app).get("/app")
35+
response = TestClient(app).get("/app", headers={"accept": "text/html"})
3636

3737
assert response.status_code == 200
3838
assert response.text == "app"
@@ -617,15 +617,21 @@ def read_users():
617617
assert response.json() == {"detail": "api missing"}
618618

619619

620-
def test_index_fallback_for_navigation_request(tmp_path: Path):
620+
@pytest.mark.parametrize(
621+
("path", "accept"),
622+
[
623+
("/dashboard/settings", "text/html"),
624+
("/users/jane.doe", "text/html"),
625+
("/v/1.2.3", "application/xhtml+xml"),
626+
],
627+
)
628+
def test_index_fallback_for_navigation_request(tmp_path: Path, path: str, accept: str):
621629
dist = tmp_path / "dist"
622630
write_file(dist / "index.html", "app shell")
623631
app = FastAPI()
624632
app.frontend("/", directory=dist, fallback="index.html")
625633

626-
response = TestClient(app).get(
627-
"/dashboard/settings", headers={"accept": "text/html"}
628-
)
634+
response = TestClient(app).get(path, headers={"accept": accept})
629635

630636
assert response.status_code == 200
631637
assert response.text == "app shell"
@@ -638,7 +644,8 @@ def test_index_fallback_parses_accept_parameters(tmp_path: Path):
638644
app.frontend("/", directory=dist, fallback="index.html")
639645

640646
response = TestClient(app).get(
641-
"/dashboard/settings", headers={"accept": "text/html; q=0.8"}
647+
"/dashboard/settings",
648+
headers={"accept": "text/html; charset=utf-8; q=0.8"},
642649
)
643650

644651
assert response.status_code == 200
@@ -697,10 +704,10 @@ def test_index_fallback_respects_explicit_xhtml_rejection_with_wildcard(
697704
("/assets/missing.css", "text/css"),
698705
("/assets/missing.png", "image/png"),
699706
("/api/missing", "application/json"),
700-
("/users/jane.doe", "text/html"),
707+
("/dashboard/settings", ""),
701708
],
702709
)
703-
def test_index_fallback_does_not_handle_asset_like_or_non_html_requests(
710+
def test_index_fallback_requires_explicit_html_acceptance(
704711
tmp_path: Path, path: str, accept: str
705712
):
706713
dist = tmp_path / "dist"

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.