You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found by the 2026-08-22 horizon-journey golden run — a 44-capability live end-to-end session against a running instance. Sibling of tracker #1947. Finding H-09.
The defect
Every non-existent /api/* route falls through to the SPA handler and returns 200 OK with the app
shell instead of a 404.
GET /api/definitely-not-a-real-endpoint-hzn
→ 200 content-type: text/html body: "<!doctype html>\n<html lang=\"en\">…"
Same for /api/captures, /api/proposals, /api/inbox, /api/workspace/review — none of which exist
(the real proposals endpoint is /api/automation/proposals).
The fallback is unscoped — it matches any unmatched path, /api/* included.
The comment directly above it (lines 165-169) asserts the opposite:
"SPA fallback: any route not matched by a controller or hub endpoint returns index.html … API (/api/*) and hub (/hubs/*) routes are matched above and never reach this fallback."
That is true only for routes a controller actually declares. An /api/* path with no matching
controller route reaches the fallback and gets a 200. So a permanent code comment records a safety
property the code does not have — worth fixing in the same change as the behaviour, since a future reader
will otherwise rely on it.
Why it matters
Any API consumer — the MCP server, scripts, tests, a future integration, or an agent probing the surface —
gets a false success for a typo'd, renamed, or removed endpoint. Client code that checks response.ok before parsing treats a 404 as a 200 and then fails on JSON parsing at a distance from the
real cause, which is exactly the silent-failure shape the recent review work has been trying to eliminate
(it was independently flagged as the enabler during the PR #1956 review).
It also makes endpoint discovery actively misleading: during this run /api/proposals looked like an
existing endpoint returning an empty result, when it simply does not exist.
Because a removed or renamed endpoint returns 200 rather than 404, contract drift is invisible to any
caller that does not parse the body — including tests written to assert "the call succeeded".
Acceptance criteria
Unknown paths under /api/ return 404 with a ProblemDetails body and content-type: application/problem+json, never text/html.
The SPA fallback is scoped so it cannot match /api/*; /hubs/* is treated the same way.
The defect
Every non-existent
/api/*route falls through to the SPA handler and returns200 OKwith the appshell instead of a 404.
Same for
/api/captures,/api/proposals,/api/inbox,/api/workspace/review— none of which exist(the real proposals endpoint is
/api/automation/proposals).Source.
backend/src/Taskdeck.Api/Extensions/PipelineConfiguration.cs:170:The fallback is unscoped — it matches any unmatched path,
/api/*included.The comment directly above it (lines 165-169) asserts the opposite:
That is true only for routes a controller actually declares. An
/api/*path with no matchingcontroller route reaches the fallback and gets a 200. So a permanent code comment records a safety
property the code does not have — worth fixing in the same change as the behaviour, since a future reader
will otherwise rely on it.
Why it matters
Any API consumer — the MCP server, scripts, tests, a future integration, or an agent probing the surface —
gets a false success for a typo'd, renamed, or removed endpoint. Client code that checks
response.okbefore parsing treats a 404 as a 200 and then fails on JSON parsing at a distance from thereal cause, which is exactly the silent-failure shape the recent review work has been trying to eliminate
(it was independently flagged as the enabler during the PR #1956 review).
It also makes endpoint discovery actively misleading: during this run
/api/proposalslooked like anexisting endpoint returning an empty result, when it simply does not exist.
Because a removed or renamed endpoint returns 200 rather than 404, contract drift is invisible to any
caller that does not parse the body — including tests written to assert "the call succeeded".
Acceptance criteria
/api/return 404 with a ProblemDetails body andcontent-type: application/problem+json, nevertext/html./api/*;/hubs/*is treated the same way./route, and deep client-side routes (/workspace/review,/workspace/boards/{id}) still serve the SPA shell — the fix must not break Vue Router deep links or the/handling added for Packaged binary: GET / returns 401 instead of the SPA shell (bare root not served under global auth fallback) #1181.FallbackPolicy(Security gate + config hardening: required secret/SAST/dependency scans, CORS fail-closed, JWT floor, FallbackPolicy, bundle gate #1132 AC4).PipelineConfiguration.cs:165-169is corrected to describe what the pipeline actually guarantees after the fix.GET /api/<nonexistent>returns 404 + ProblemDetails, asserted for both an authenticated and an unauthenticated caller./hubs/<nonexistent>does not return the SPA shell.Refs