fix: validate request body in POST /projects/:projectId/chat
From the PR description
Summary
projectChat.ts:32-39 reads `req.body` via a bare `as { ... }` cast, while sibling routes (`chat.ts`, `user.ts`) use hand-written parsers. A misshapen body - wrong types for `messages`, `displayed_doc`, `attached_documents` - therefore reaches the DB writes and the LLM call.
Changes
- Add per-field parsers in `backend/src/routes/projectChat.ts` matching the `parseFoo(value: unknown): { ok: true; ... } | { ok: false; detail }` pattern already used by `routes/chat.ts`: `parseChatMessages`, `parseOptionalChatId`, `parseOptionalModel`, `parseDocRef`, `parseOptionalDisplayedDoc`, `parseOptionalAttachedDocuments`.
- Validate every field before any DB or LLM work; return `400` with the parser's `detail` message on the first failure.
- Downstream variable names (`messages`, `chat_id`, `model`, `displayed_doc`, `attached_documents`) are preserved so the rest of the handler is unchanged.
Why
Concrete failures the previous cast permitted:
- `messages` as a non-array → `[...messages].reverse()` throws inside the handler after the chat row has already been inserted.
- `attached_documents` containing non-object entries → `.map((d) => ...)` crashes on `d.document_id`.
- `displayed_doc` as a string → the route builds a malformed user message and sends it to the model.
Matches the existing style rather than introducing a new validation library, per CONTRIBUTING.md guidance to keep changes targeted.
Testing
- `npm run build --prefix backend` passes.
- Walked each parser through the previously crash-inducing shapes and confirmed each now returns a 400 with a useful `detail`.
Addresses the input-validation concern from https://insights.flank.ai/where-mikeoss-falls-short.html (gap 7) for this specific route. Other routes already validate by hand.
Our analysis
Add hand-written body validation to the project chat route — read the full analysis →
Think the analysis missed something the PR description covers?
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-155.md from
inside the repo you want the changes in.