From eac2752227ea75e43a9bccacc71c1b2c62ad1b5f Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 7 Jun 2026 03:38:57 +0000 Subject: [PATCH] Return 400 for malformed document id instead of 500 (PAP-10582) A non-UUID :documentId reached a uuid-typed equality query and surfaced Postgres's "invalid input syntax for type uuid" as an opaque 500. Add a router.param guard that rejects malformed ids early with a structured 400, covering every :documentId route at once. Co-Authored-By: Claude Opus 4.8 --- server/src/__tests__/documents-routes.test.ts | 24 +++++++++++++++++++ server/src/routes/documents.ts | 12 ++++++++++ 2 files changed, 36 insertions(+) diff --git a/server/src/__tests__/documents-routes.test.ts b/server/src/__tests__/documents-routes.test.ts index ad7601c5c89..c40c4efefc6 100644 --- a/server/src/__tests__/documents-routes.test.ts +++ b/server/src/__tests__/documents-routes.test.ts @@ -158,4 +158,28 @@ describeEmbeddedPostgres("document routes", () => { }), ])); }); + + it("returns 400 for a malformed document id instead of a 500 (PAP-10582)", async () => { + const companyA = await seedCompany("A"); + const app = createApp(); + + const res = await request(app) + .get(`/api/companies/${companyA}/documents/not-a-uuid`) + .expect(400); + expect(res.body).toEqual({ error: "Invalid document id format" }); + + // Other :documentId routes are covered by the same param guard. + await request(app) + .get(`/api/companies/${companyA}/documents/not-a-uuid/backlinks`) + .expect(400); + await request(app) + .patch(`/api/companies/${companyA}/documents/not-a-uuid`) + .send({ status: "archived" }) + .expect(400); + + // A well-formed but non-existent id still resolves to a 404, not a 400. + await request(app) + .get(`/api/companies/${companyA}/documents/${randomUUID()}`) + .expect(404); + }); }); diff --git a/server/src/routes/documents.ts b/server/src/routes/documents.ts index 5a40079fcb8..20f439ce534 100644 --- a/server/src/routes/documents.ts +++ b/server/src/routes/documents.ts @@ -3,6 +3,7 @@ import type { Db } from "@paperclipai/db"; import { companyDocumentListQuerySchema, createDocumentLinkSchema, + isUuidLike, updateDocumentMetadataSchema, } from "@paperclipai/shared"; import { validate } from "../middleware/validate.js"; @@ -15,6 +16,17 @@ export function documentRoutes(db: Db) { const projects = projectService(db); const access = accessService(db); + // Document ids are UUID-keyed. Reject malformed ids early so they surface as a + // structured 400 instead of bubbling up as a Postgres "invalid input syntax for + // type uuid" error (which would become an opaque 500). See PAP-10582. + router.param("documentId", (req, res, next, value) => { + if (!isUuidLike(value)) { + res.status(400).json({ error: "Invalid document id format" }); + return; + } + next(); + }); + router.get("/companies/:companyId/documents", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId);