Unknown /api paths returned the SPA with HTTP 200

An authenticated GET to any unrecognised API route — a typo, a renamed
endpoint, an older client — fell through to the SPA fallback and returned
200 text/html containing the app shell.

This is close to the worst failure shape for an API consumer. `response.ok` is
true, so nothing treats it as an error; the caller then dies on `JSON.parse`
with "Unexpected token '<'" far from the actual cause. The MCP server, the CLI
and Piggy all consume this API and would all have hit it. It was masked from
casual testing because unauthenticated requests are rejected earlier by the
auth middleware, so it only appears once you hold a valid token.

Found by probing production with Scott's token: GET /api/keys (the real path is
/api/api-keys) returned 200 text/html.

The static-file middleware already carried this guard — added for the same
reason when og.png was being served as HTML — but the SPA fallback beneath it
did not. Same guard, one place missing.

Verified: unknown API paths now return 404 application/json, real API paths
still answer, client-side routes still receive the shell, and static assets
still serve with their own content types. Typecheck clean, 124 unit tests and
the e2e suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 02:05:00 -07:00
parent 6bd5526675
commit 54edee30ed
2 changed files with 26 additions and 3 deletions
+19 -3
View File
@@ -57,9 +57,25 @@ if (existsSync(webDist)) {
return serveStatic({ root: './apps/web/dist' })(c, next);
});
// Client-side routes (/margin, /capacity, …) have no file on disk and must
// receive the shell so the router can take over.
app.get('*', serveStatic({ path: './apps/web/dist/index.html' }));
/*
* Client-side routes (/margin, /capacity, …) have no file on disk and must
* receive the shell so the router can take over.
*
* The `/api/` guard is repeated here deliberately. Without it an unknown API
* path — a typo, a renamed endpoint, an older client — falls through to this
* fallback and returns **HTTP 200 with the SPA's HTML**. That is close to the
* worst possible failure for an API consumer: `response.ok` is true, so
* nothing treats it as an error, and the caller then fails on `JSON.parse`
* with "Unexpected token '<'" a long way from the actual cause. The MCP
* server, the CLI and Piggy all consume this API and would all have hit it.
*
* Confirmed against production before fixing: an authenticated GET to
* /api/keys (the real path is /api/api-keys) returned 200 text/html.
*/
app.get('*', async (c, next) => {
if (new URL(c.req.url).pathname.startsWith('/api/')) return next();
return serveStatic({ path: './apps/web/dist/index.html' })(c, next);
});
console.log('[pig] serving front end from', webDist);
}