Summary
The published self-contained desktop binary serves the SPA for every path except the bare root /, which returns HTTP 401 ({"errorCode":"Unauthorized"}) instead of index.html. / is the URL the first-run flow logs and the browser auto-opens to, so a user navigating to http://localhost:5000 gets a JSON 401 error on first launch.
Discovered while implementing the #1123 AC2 post-publish smoke test (release-desktop.yml). The AC2 smoke asserts GET / returns the SPA, so this currently blocks a green release run (the gate working as intended).
Reproduction (local, win-x64 self-contained single-file publish)
Run the published exe (cwd = publish dir) with throwaway secrets, then:
GET /index.html -> 200 (static files serve the shell)
GET /board -> 200 (MapFallbackToFile SPA fallback, AllowAnonymous)
GET /login -> 200 (SPA fallback)
GET / -> 401 (bare root)
GET /health/ready -> 200
Reproduces identically in both ASPNETCORE_ENVIRONMENT=Production (default) and Development, so it is environment-independent. The packaged SPA-serving path has never shipped, so this was never exercised before.
Root cause
backend/src/Taskdeck.Api/Extensions/PipelineConfiguration.cs uses UseDefaultFiles() + UseStaticFiles() + app.MapFallbackToFile("index.html").AllowAnonymous(). The SPA fallback's catch-all route ({*path:nonfile}) does not match the empty root path, so / matches no endpoint and is rejected by the global SetFallbackPolicy(RequireAuthenticatedUser) (#1132 AC4) -> 401. Non-empty client routes (/board) do match the fallback and are served. UseDefaultFiles is not rewriting / -> /index.html in this configuration.
Existing coverage gap: backend/tests/Taskdeck.Api.Tests/FallbackPolicyTests.cs only exercises a non-empty route (/some/client/route); the bare root / is untested.
Recommended fix
Ensure the bare root serves the SPA shell anonymously, e.g. add alongside the existing fallback:
// Bare root must serve the SPA shell; the {*path:nonfile} fallback doesn't match the empty path.
app.MapFallbackToFile("/", "index.html").AllowAnonymous();
(or otherwise make / resolve to index.html). Add a GET / -> 200 + index.html regression test to FallbackPolicyTests mirroring the existing SPA-fallback test.
Impact
Summary
The published self-contained desktop binary serves the SPA for every path except the bare root
/, which returns HTTP 401 ({"errorCode":"Unauthorized"}) instead ofindex.html./is the URL the first-run flow logs and the browser auto-opens to, so a user navigating tohttp://localhost:5000gets a JSON 401 error on first launch.Discovered while implementing the #1123 AC2 post-publish smoke test (release-desktop.yml). The AC2 smoke asserts
GET /returns the SPA, so this currently blocks a green release run (the gate working as intended).Reproduction (local, win-x64 self-contained single-file publish)
Run the published exe (cwd = publish dir) with throwaway secrets, then:
Reproduces identically in both
ASPNETCORE_ENVIRONMENT=Production(default) andDevelopment, so it is environment-independent. The packaged SPA-serving path has never shipped, so this was never exercised before.Root cause
backend/src/Taskdeck.Api/Extensions/PipelineConfiguration.csusesUseDefaultFiles()+UseStaticFiles()+app.MapFallbackToFile("index.html").AllowAnonymous(). The SPA fallback's catch-all route ({*path:nonfile}) does not match the empty root path, so/matches no endpoint and is rejected by the globalSetFallbackPolicy(RequireAuthenticatedUser)(#1132 AC4) -> 401. Non-empty client routes (/board) do match the fallback and are served.UseDefaultFilesis not rewriting/->/index.htmlin this configuration.Existing coverage gap:
backend/tests/Taskdeck.Api.Tests/FallbackPolicyTests.csonly exercises a non-empty route (/some/client/route); the bare root/is untested.Recommended fix
Ensure the bare root serves the SPA shell anonymously, e.g. add alongside the existing fallback:
(or otherwise make
/resolve to index.html). Add aGET /-> 200 + index.html regression test to FallbackPolicyTests mirroring the existing SPA-fallback test.Impact
/) shows a 401 JSON error.