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