-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Serve SPA fallback with 200 for routable paths in prod static serving #6996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
139ba3c
38f3789
319b3eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Serve valid dynamic-route URLs (e.g. `/articles/7`) with HTTP 200 instead of 404 when loaded directly in self-hosted prod static serving, reserving 404 for genuinely unknown paths. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add the `routes.json` manifest name constant, written at compile time so the prod static file server can tell routable SPA paths from unknown ones. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |||||||||||
|
|
||||||||||||
| import os | ||||||||||||
| import stat | ||||||||||||
| from collections.abc import Sequence | ||||||||||||
| from collections.abc import Callable, Sequence | ||||||||||||
| from dataclasses import dataclass | ||||||||||||
| from functools import lru_cache | ||||||||||||
| from mimetypes import guess_type | ||||||||||||
|
|
@@ -72,17 +72,23 @@ def __init__( | |||||||||||
| self, | ||||||||||||
| *args, | ||||||||||||
| encodings: Sequence[str] = (), | ||||||||||||
| router: Callable[[str], str | None] | None = None, | ||||||||||||
| **kwargs, | ||||||||||||
| ): | ||||||||||||
| """Initialize the static file server. | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| *args: Passed through to ``StaticFiles``. | ||||||||||||
| encodings: Ordered list of supported precompressed formats. | ||||||||||||
| router: Optional route matcher taking the request path (with leading | ||||||||||||
| slash) and returning the matching app route, or ``None``. Paths | ||||||||||||
| with no on-disk file that match a route are served the SPA | ||||||||||||
| fallback with status 200 instead of 404. | ||||||||||||
| **kwargs: Passed through to ``StaticFiles``. | ||||||||||||
| """ | ||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||
| self._encodings = tuple(_SUPPORTED_ENCODINGS[name] for name in encodings) | ||||||||||||
| self._router = router | ||||||||||||
|
|
||||||||||||
| def _select_sidecar( | ||||||||||||
| self, full_path: str | PathLike[str], scope: Scope | ||||||||||||
|
|
@@ -178,16 +184,24 @@ async def get_response(self, path: str, scope: Scope) -> Response: | |||||||||||
| The resolved static response for the request. | ||||||||||||
| """ | ||||||||||||
| response = await super().get_response(path, scope) | ||||||||||||
| # Starlette's get_response builds the 404.html fallback with bare FileResponse, | ||||||||||||
| # bypassing file_response. Re-route it so the sidecar/Vary handling applies. | ||||||||||||
| if ( | ||||||||||||
| self._encodings | ||||||||||||
| and self.html | ||||||||||||
| self.html | ||||||||||||
| and isinstance(response, FileResponse) | ||||||||||||
| and response.status_code == 404 | ||||||||||||
| and response.stat_result is not None | ||||||||||||
| ): | ||||||||||||
| return self.file_response( | ||||||||||||
| response.path, response.stat_result, scope, status_code=404 | ||||||||||||
| ) | ||||||||||||
| # SPA fallback: a path with no prerendered file that still matches | ||||||||||||
| # the app's route table is a valid page, so serve it with 200 and | ||||||||||||
| # reserve 404 for genuinely unknown paths. | ||||||||||||
| if self._router is not None and self._router("/" + path) is not None: | ||||||||||||
|
FarhanAliRaza marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking on Windows: Restoring the URL form before matching fixes it:
Suggested change
Regression tests (one driving the ASGI callable so Windows CI exercises the real Generated by Claude Code |
||||||||||||
| return self.file_response( | ||||||||||||
| response.path, response.stat_result, scope, status_code=200 | ||||||||||||
| ) | ||||||||||||
| # Starlette's get_response builds the 404.html fallback with bare | ||||||||||||
| # FileResponse, bypassing file_response. Re-route it so the | ||||||||||||
| # sidecar/Vary handling applies. | ||||||||||||
| if self._encodings: | ||||||||||||
| return self.file_response( | ||||||||||||
| response.path, response.stat_result, scope, status_code=404 | ||||||||||||
| ) | ||||||||||||
| return response | ||||||||||||
Uh oh!
There was an error while loading. Please reload this page.