Answer 404 for an id that cannot name a row, rather than 500
Measured against a running server: five of the eleven Motion `:id` routes
answered `500 {"error":"Internal error"}` for an id like `nope`, and the
other six only answered 400 because their body schema happened to be
checked first — a valid body would have reached the same cast.
Nothing was wrong with the not-found handling. That branch was never
reached: every id column is a `uuid`, so Postgres refuses the parameter
with `22P02` several layers below it, and the error is not a
MutationError so it leaves as a 500.
404 rather than 400, because a 400 for a malformed id and a 404 for a
well-formed one tells anyone probing which of their guesses are the right
shape — and this feature already routes "somebody else's private draft"
through the same 404 so that no answer distinguishes the reasons a row is
not yours to see.
Also adds the AGENTS.md §5 393px check for the five Motion routes, which
scripts/screenshots.mjs does not photograph. All five measure zero
horizontal overflow at 393 and 1440, light and dark.
The same 500 is reachable on /api/accounts/:id and /api/contracts/:id,
which predates this branch and is left alone here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,22 @@ import { MutationError } from '../lib/mutation';
|
||||
|
||||
export type MotionTransaction = Parameters<Parameters<Database['transaction']>[0]>[0];
|
||||
|
||||
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
/**
|
||||
* Whether a string can name a row at all.
|
||||
*
|
||||
* Every id here is a `uuid` column, so a lookup by `nope` never reaches the
|
||||
* "no such row" branch: Postgres refuses the cast with `22P02` and the request
|
||||
* leaves as a 500. The reads below therefore answer null for a malformed id,
|
||||
* which the routes already turn into the 404 an unknown id gets — the same
|
||||
* answer a private template gives, which is the point of routing both through
|
||||
* one branch. `requiredId` in `routes/motion.ts` does the same for the writes.
|
||||
*/
|
||||
function isUuid(id: string): boolean {
|
||||
return UUID.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The subset of a `Principal` a motion read is allowed to see. A service that
|
||||
* took the whole principal would be one refactor away from consulting teams or
|
||||
@@ -519,6 +535,7 @@ export class MotionService {
|
||||
* produces, so a private title cannot be confirmed by probing for one.
|
||||
*/
|
||||
async template(viewer: MotionViewer, id: string): Promise<MotionTemplateDetailView | null> {
|
||||
if (!isUuid(id)) return null;
|
||||
const [row] = await this.db
|
||||
.select()
|
||||
.from(motionTemplates)
|
||||
@@ -544,6 +561,7 @@ export class MotionService {
|
||||
}
|
||||
|
||||
async engagement(viewer: MotionViewer, id: string): Promise<MotionEngagementDetail | null> {
|
||||
if (!isUuid(id)) return null;
|
||||
const [summary] = await this.engagementSummaries(eq(engagements.id, id), 1);
|
||||
if (!summary) return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user