From fc5c6ffed2d35120f74309ebca2a7e6d807eb06c Mon Sep 17 00:00:00 2001 From: Christian Lappin <36496308+christianlappin@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:08:49 -0400 Subject: [PATCH] fix(server): return 404 instead of 500 for non-UUID company refs (#9959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The board REST API is how operators and integrations read company state; `GET /api/companies/:companyId` is one of its most basic reads > - The route passes the raw path param into `companyService.getById`, which queries the uuid-typed `companies.id` column directly > - Any non-UUID ref — a slug, a typo, a stale bookmark — makes Postgres throw `invalid input syntax for type uuid`, which surfaces as an HTTP 500 with a stack trace in the server log instead of a clean client error > - A 500 for malformed client input is miscategorized: it pages operators, pollutes error budgets, and hides the actual problem ("that ref doesn't exist") from the caller > - This pull request guards `getById` with a UUID check so non-UUID refs resolve to `null` and the route returns its existing 404 path > - The benefit is correct HTTP semantics for bad input, quieter logs, and one less misleading 500 for self-hosters to chase ## Linked Issues or Issue Description Fixes #9962 — `GET /api/companies/:companyId` returns 500 (`invalid input syntax for type uuid`) for non-UUID refs instead of 404. Full repro and log excerpt in the issue. ## What Changed - `server/src/services/companies.ts`: `getById` returns `null` early for non-UUID refs instead of passing them to the uuid-typed query. - `server/src/__tests__/companies-service.test.ts`: regression test — non-UUID refs (`"tumbly-haus-creative"`, `"not-a-uuid"`, `""`) resolve to `null` without a query error. ## Verification - `npx vitest run src/__tests__/companies-service.test.ts` — 12/12 pass (new test included, embedded-postgres suite). - Manual: `curl -i /api/companies/not-a-uuid` → 404 (was 500); `curl -i /api/companies/` → 200 unchanged. ## Risks - Low. Pure input-validation guard on one read path; UUID lookups are byte-for-byte unchanged. Only behavioral shift is 500→404 for refs that could never have matched a row. ## Model Used Claude Fable 5 (`claude-fable-5`, Anthropic) via Claude Code — diagnosis from server logs, patch, and test authored with extended thinking and tool use; human-reviewed and submitted by @christianlappin. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal ticket id - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes (n/a — no doc references this error path) - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green (pending first CI run) - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups (pending) - [x] I will address all Greptile and reviewer comments before requesting merge --- server/src/__tests__/companies-service.test.ts | 8 ++++++++ server/src/services/companies.ts | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/server/src/__tests__/companies-service.test.ts b/server/src/__tests__/companies-service.test.ts index e3aa0c7ae4..d834695484 100644 --- a/server/src/__tests__/companies-service.test.ts +++ b/server/src/__tests__/companies-service.test.ts @@ -869,4 +869,12 @@ describeEmbeddedPostgres("companyService", () => { details: { agentsPaused: 1, runsCancelled: 1 }, }); }); + + it("getById returns null (not a query error) for non-UUID refs", async () => { + const svc = companyService(db); + await expect(svc.getById("tumbly-haus-creative")).resolves.toBeNull(); + await expect(svc.getById("not-a-uuid")).resolves.toBeNull(); + await expect(svc.getById("")).resolves.toBeNull(); + }); + }); diff --git a/server/src/services/companies.ts b/server/src/services/companies.ts index c3d03ebfcd..2105650a1c 100644 --- a/server/src/services/companies.ts +++ b/server/src/services/companies.ts @@ -39,6 +39,8 @@ import { heartbeatService } from "./heartbeat.js"; import { logActivity } from "./activity-log.js"; import { builtInAgentService } from "./built-in-agents.js"; + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export interface CompanyActivityActor { actorType: "user" | "agent" | "system" | "plugin"; actorId: string; @@ -259,6 +261,11 @@ export function companyService(db: Db) { }, getById: async (id: string) => { + // Non-UUID refs previously reached the uuid-typed query and threw a + // DrizzleQueryError ("invalid input syntax for type uuid"), surfacing + // as HTTP 500 from GET /api/companies/:companyId. Treat them as + // not-found so the route returns 404. + if (!UUID_RE.test(id)) return null; const row = await getCompanyQuery(db) .where(eq(companies.id, id)) .then((rows) => rows[0] ?? null);