tRPC server returns HTTP 200 for every error, including UNAUTHORIZED #22

Open
opened 2026-07-31 20:38:07 +00:00 by forgejo-admin · 0 comments

responseMeta in apps/lunacycle-server/src/index.ts hard-codes status: 200 on every tRPC response, so error envelopes are served with a success status.

Reproduce against production:

curl -H 'Authorization: Bearer garbage' \
  'https://www.lunacycle.net/api/getActiveMonth?batch=1&input=%7B%7D'

-> HTTP 200
   [{"error":{"message":"Authentication required","code":-32001,
     "data":{"code":"UNAUTHORIZED","httpStatus":401,...

Note the body itself carries httpStatus: 401 — tRPC computed the correct status and responseMeta discarded it.

Impact. Any client that checks the HTTP status cannot distinguish success from auth failure or a server error. This is not hypothetical: it made the production smoke_api health check unfalsifiable, reporting ✓ API responded (HTTP 200) while the server logged ERR_JWS_INVALID for that exact request. Found in #20; the smoke checks now work around it by asserting on the response body instead (assertNoTrpcError in scripts/smoke/api-smoke.ts), but the underlying behavior is unchanged for every other consumer — monitoring, proxies, browser devtools, and any future client.

Fix. Return the status tRPC already computed rather than a constant. The headers block in the same responseMeta is doing real work (CORS + security headers) and should stay.

Watch out for. The SPA may have grown code that assumes 200-always — worth grepping before changing, and worth checking whether Caddy or any monitoring keys off the status.

Found while investigating the first-ever production run of the smoke checks (#20).

`responseMeta` in `apps/lunacycle-server/src/index.ts` hard-codes `status: 200` on every tRPC response, so error envelopes are served with a success status. Reproduce against production: ``` curl -H 'Authorization: Bearer garbage' \ 'https://www.lunacycle.net/api/getActiveMonth?batch=1&input=%7B%7D' -> HTTP 200 [{"error":{"message":"Authentication required","code":-32001, "data":{"code":"UNAUTHORIZED","httpStatus":401,... ``` Note the body itself carries `httpStatus: 401` — tRPC computed the correct status and `responseMeta` discarded it. **Impact.** Any client that checks the HTTP status cannot distinguish success from auth failure or a server error. This is not hypothetical: it made the production `smoke_api` health check unfalsifiable, reporting `✓ API responded (HTTP 200)` while the server logged `ERR_JWS_INVALID` for that exact request. Found in #20; the smoke checks now work around it by asserting on the response body instead (`assertNoTrpcError` in `scripts/smoke/api-smoke.ts`), but the underlying behavior is unchanged for every other consumer — monitoring, proxies, browser devtools, and any future client. **Fix.** Return the status tRPC already computed rather than a constant. The headers block in the same `responseMeta` is doing real work (CORS + security headers) and should stay. **Watch out for.** The SPA may have grown code that assumes 200-always — worth grepping before changing, and worth checking whether Caddy or any monitoring keys off the status. Found while investigating the first-ever production run of the smoke checks (#20).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
celilo/lunacycle#22
No description provided.