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:
@@ -165,6 +165,13 @@ Verified: 1× A100 at 1.79, 2× A100 at 3.58. `gpuMemory` is likewise a node
|
||||
total. There is an open bug for this — the mapper currently stores both as if
|
||||
per-GPU, so an 8-GPU node reads eight times too expensive.
|
||||
|
||||
**The SPA fallback must never answer an `/api/` path.** Without an explicit
|
||||
guard, an unknown API route returns `200 text/html` — the app shell — and the
|
||||
caller sees `response.ok === true` before failing on `JSON.parse` with
|
||||
"Unexpected token '<'", a long way from the cause. Both the static-file
|
||||
middleware and the SPA fallback in `apps/api/src/server.ts` carry the guard;
|
||||
anything added after them needs it too.
|
||||
|
||||
**Prime Intellect has two API hosts.** `api.primeintellect.ai` is compute and
|
||||
pods. Inference is `api.pinference.ai/api/v1`, OpenAI-compatible.
|
||||
|
||||
|
||||
+19
-3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user