From 54edee30ed61f3e2419e94f134f72313fc052472 Mon Sep 17 00:00:00 2001 From: karti Date: Thu, 13 Aug 2026 02:05:00 -0700 Subject: [PATCH] Unknown /api paths returned the SPA with HTTP 200 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- AGENTS.md | 7 +++++++ apps/api/src/server.ts | 22 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 47ad03e..6d69968 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 3c510bc..46ec14b 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -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); }