Repository navigation
DRU-1149 -- SPA fallback breaks on FastAPI 0.162: Accept: */* no longer serves index.html - #1169
Conversation
FastAPI serves the index.html fallback only to a navigation request. FastAPI 0.162 no longer counts a bare Accept: */* as one. The two /work_items/42 requests now send Accept: text/html, as a browser navigation does. The serve_spa docstring states this requirement. The test module imports Session at module scope. Co-Authored-By: paulo@druks.ai <paulo@druks.ai>
cff5e3c to
2becf44
Compare
There was a problem hiding this comment.
Verdict: approve, with one non-blocking finding (comment).
The PR does what it says, and all five acceptance criteria hold. Both /work_items/42 requests send Accept: text/html and keep their content and no-cache assertions. The Session import moved to module scope. The serve_spa docstring is now in short sentences, states the navigation requirement, and names no FastAPI version. Nothing else changed.
What I checked:
- FastAPI source. In 0.162.0,
_is_frontend_navigation_request(fastapi/routing.py) serves the index.html fallback only when Accept liststext/htmlorapplication/xhtml+xmlwith q > 0. In 0.139.0 it also accepted*/*. The new docstring sentence matches the 0.162 rule. - PR #1165 CI. All four backend jobs fail on these two tests and nothing else (
2 failed, 2634 passed). - Local runs against Postgres. The updated file passes on the locked 0.139.0 and on 0.162.0 (5 passed each time). The file from
mainfails the same two tests on 0.162.0. - review.md tell-greps on both touched files. Clean. The remaining
_-prefixed defs inserver.pyare exception handlers and a lifespan callback, so they are framework callbacks, not one-caller helpers. - Production callers. Caddy passes
Acceptthrough. Browser navigations and the OAuth redirect to/app/<name>/...sendtext/html. The deploy health checks hit/health, not a client route. No production client relies on a*/*request getting the fallback.
One finding, inline on test_api_paths_win_over_the_spa. After the 0.162 bump, this test passes without the catch-all it is meant to protect. The fix is the same one-header change that this PR applies to the other two tests.
| assert "druks" in client.get("/work_items/42", headers={"Accept": "text/html"}).text | ||
|
|
||
|
|
||
| def test_api_paths_win_over_the_spa(tmp_path): |
There was a problem hiding this comment.
medium. After the 0.162 bump, this test passes without the code it protects. TestClient sends Accept: */*. On 0.162, FastAPI never serves the index.html fallback for that header. So /api/nope returns 404 whether the /api/{path:path} catch-all exists or not.
I checked this: I deleted api_not_found from _app_with_spa and ran the file. On 0.139.0, this test fails, as it should. On 0.162.0, all 5 tests pass. The case the catch-all protects, which the comment on line 40 describes, is a browser navigation to an unknown /api/* path. Line 41 needs the same header as the client-route requests:
assert client.get("/api/nope", headers={"Accept": "text/html"}).status_code == 404This goes past AC2, which kept this test unchanged. But it has the same cause and the same fix as AC1, and review.md item 14 requires a test to fail when the code it guards is gone. With the header, the test fails without the catch-all on both 0.139 and 0.162.
What changed
A browser navigation to a dashboard client route still gets index.html. FastAPI serves that fallback only to a navigation request. FastAPI 0.162 no longer counts a bare
Accept: */*as one, andTestClientsends*/*by default. The two/work_items/42requests inbackend/tests/test_spa_serving.pynow sendAccept: text/html, as a browser navigation does. Theserve_spadocstring states that the index.html fallback needs that header. The test module also importsSessionat module scope.On FastAPI 0.162.0, the unchanged tests get
{"detail":"Not Found"}for/work_items/42and fail. The updated file passes on the locked 0.139.0 and on 0.162.0, so the Dependabot bump in PR #1165 can land green. The fix stays in the tests and the docstring. The FastAPI pin, theapp.frontend()arguments,SpaCacheControl, and the/api/*JSON 404 catch-all do not change.Plan
Make one small PR correcting the SPA navigation requests and documenting their requirement.
Verified at HEAD: the two named tests in
backend/tests/test_spa_serving.pyrequest/work_items/42without a navigation header.serve_spacallsapp.frontend("/", directory=dist), anduv.lockcontains FastAPI 0.139.0. The full ticket and its one existing understanding comment were read; no operator refinement changes the requested approach. The ticket has no assignee.Accept: text/htmlindividually to the/work_items/42requests intest_spa_serves_index_assets_and_client_routesandtest_cache_policy_lives_with_the_server. Preserve their content and cache assertions. Keep the other requests, behavior, and assertions unchanged.from sqlalchemy.orm import Sessionfrom the database-session test to module scope. This addresses the function-level-import tell required by.druks/software_factory/review.md, without changing the test's behavior.backend/druks/api/server.py, split the openingserve_spadocstring explanation into short sentences. Add the requested one-line navigation requirement as one short active sentence in the repository's simplified technical English. Describe only the end-state requirement: the index.html fallback requires a navigation request with anAccept: text/htmlheader. Do not mention FastAPI versions or before/after behavior. Any test comment added must likewise explain an end-state reason..druks/software_factory/review.md. Registered exception handlers are framework callbacks, not one-caller helper abstractions.Verification:
backend/tests/conftest.pyhas a session-scoped autouse_reset_test_databasefixture, so even this focused file needs Postgres. Usedruks.testing.TEST_DATABASE_URL, which readsDRUKS_TEST_DATABASE_URLand defaults to the localdruks_testdatabase. If a matching database is reachable or can be started usingdeploy/compose.dev.yaml, run the focused file with the locked environment and withuv run --with fastapi==0.162.0 pytest backend/tests/test_spa_serving.py. The temporary override must leave dependency declarations and lockfiles unchanged. If Postgres cannot be made available, report the exact command and blocker asnot_run; do not bypass or alter the fixtures.As a database-free fallback, use an uncommitted throwaway script outside the repository under
uv run --with fastapi==0.162.0. Set a temporary validDRUKS_SECRETS_KEYbefore importingdruks.api.server. Construct the SPA app as_app_with_spadoes and addSpaCacheControl. Check that GET/work_items/42with bareAccept: */*returns 404; withAccept: text/htmlit returns 200, index.html, andcache-control: no-cache; and unknown/api/*paths return JSON 404 with either header. Record this as isolated compatibility evidence, not a substitute for the focused suite. PR #1165's CI is the real FastAPI 0.162 gate.Scope boundary: “Do not cap the FastAPI version — the goal is to let the 0.162 bump (PR #1165) land green.” The dependency bump remains separate. This PR changes tests and a docstring only. No implementation edits or test runs were performed during this planning pass.
Acceptance Criteria