nginx: only serve the SPA shell to browser navigation - #19
Open
albertotb wants to merge 1 commit into
Open
Conversation
The SPA fallback answered every unmatched path with index.html and a 200, including missing assets and mistyped fetch() paths — a "not found" then surfaces as a parse error somewhere downstream instead of a 404. Restrict the fallback to GET/HEAD requests that accept HTML, which is what browsers send when navigating; everything else keeps its 404. This is the rule FastAPI's app.frontend() applies, so a stack that later serves the build from FastAPI behaves the same way. The Docker job now asserts both halves of the rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011texLkDBELWbXsBf6San3M
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tightens the SPA fallback, taking the semantics from FastAPI's new
app.frontend().The bug
try_files $uri $uri/ /index.htmlanswers every unmatched path with the HTML shell and a200. So a mistyped bundle path or afetch('/typo')gets HTML where JS or JSON was expected, and the real problem ("that path doesn't exist") only shows up later as a parse error./assets/*was already safe — thatlocationblock has notry_files— but nothing else was.The rule
Only
GET/HEADrequests that accept HTML get the shell; everything else keeps its 404. That is what browsers send when navigating, and it is the same ruleapp.frontend()uses, so a project that later serves the build from FastAPI instead of nginx behaves identically.The
add_headeris repeated because the fallback is served from@spadirectly and so does not pass throughlocation = /index.html. Verified:/aboutstill comes backno-cache, and hashed assets keepimmutable.Verified against real nginx
Both configs, in the
nginxinc/nginx-unprivileged:1.29-alpineimage, with adist-shaped tree — andnginx.compose.confagainst a stand-in backend so the/apiproxy was live:GET /GET /about(Accept: text/html)GET /nested/deep/link(browser Accept)GET /assets/missing.jsGET /typo.json(Accept: application/json)GET /missing.png(Accept: image/*)POST /aboutGET /api/health(proxy)GET /api/missing(proxied 404)One deliberate behaviour change to know about:
curl http://host/aboutwith noAcceptheader now returns 404, because*/*is not a request for HTML — that is exactly the case (XHR/fetchdefaults) the fallback should not swallow. Add-H 'Accept: text/html'when testing deep links by hand.curl http://host/is unaffected:/is served by theindexdirective, not the fallback, so the Docker job's existing check still passes.CI
The Docker job now asserts both halves of the rule — a client route returns the shell, a missing asset returns 404 — so the fallback cannot silently loosen again.
Not included
This does not adopt
app.frontend()itself. That would need the API moved off/(it collides with the SPA entry point, since path operations win) and the frontend build copied into the backend image — both inbackend/, which is a rendered mirror ofpython-copier-template. Borrowing the fallback rule is the part that stands alone.🤖 Generated with Claude Code
https://claude.ai/code/session_011texLkDBELWbXsBf6San3M
Generated by Claude Code