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); }