From 505e7b40fcc747f31ab6f1c61f5fdf8f292f5ee6 Mon Sep 17 00:00:00 2001 From: Maxxsong7 <134149388+Maxxsong7@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:47:19 +0900 Subject: [PATCH] fix: guard listComments against non-UUID afterCommentId to prevent 500 errors (#8695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is an open-source app for managing AI agents > - The issue history subsystem stores comments per issue, with cursor-based pagination via the `after` query parameter > - `GET /issues/:id/comments?after=` looks up the anchor comment by UUID to get its created_at timestamp > - When agents store an incorrect or truncated comment ID (e.g. `670427ab` instead of `670427ab-e0ae-4a54-959e-2b13a2e33d14`), Postgres throws `invalid input syntax for type uuid` before the anchor-not-found guard can execute > - This surfaces as an unhandled 500 and causes agents to fail when doing incremental comment reads on any issue > - This pull request adds a UUID validation guard in `listComments` using the already-imported `isUuidLike` helper > - The benefit is that invalid cursors get a clean empty-array response instead of a 500, matching what already happens when a valid UUID simply isn't found ## Linked Issues or Issue Description Refs #2612 (a different 500 on the same `after=` cursor path, fixed earlier; this PR covers the malformed-cursor case that remains). **What happened?** `GET /issues/:id/comments?after=` returns a 500 when `after` is not a UUID. The route trims the query value and passes it straight to the anchor lookup, so Postgres raises `invalid input syntax for type uuid: "670427ab"` before the anchor-not-found guard can run. Any agent that stored a truncated or malformed comment ID as its pagination cursor gets stuck in a 500 loop on that issue. **Expected behavior** A cursor that cannot name a comment behaves like a cursor that names a missing comment: the endpoint returns `[]`. **Steps to reproduce** 1. Pick any issue id on a running instance. 2. Call `GET /api/issues//comments?after=670427ab` (8 hex characters instead of a full UUID). 3. Observe a 500 with `PostgresError: invalid input syntax for type uuid: "670427ab"`, where a full-but-unknown UUID such as `00000000-0000-0000-0000-000000000000` returns `[]`. **Paperclip version or commit** `master` at the time this PR was opened (June 2026). The `listComments` anchor lookup in `server/src/services/issues.ts` is unchanged on current `master`, so the failure still reproduces there. **Deployment mode** Local dev (`pnpm dev`). Not deployment-specific: the failure is in the server's comment-listing service, so it reproduces in every mode. ## What Changed - `server/src/services/issues.ts` — added `if (!isUuidLike(afterCommentId)) return [];` guard in `listComments` before the DB anchor lookup, using the already-imported `isUuidLike` helper ## Verification ```bash # Start the dev server pnpm dev # Pass a truncated UUID — should return [] instead of 500 curl -s "http://localhost:3100/api/issues//comments?after=670427ab" # Expected: [] # Pass a valid full UUID that doesn't exist — should also return [] curl -s "http://localhost:3100/api/issues//comments?after=00000000-0000-0000-0000-000000000000" # Expected: [] # Pass a valid full UUID that exists — should return comments after that cursor curl -s "http://localhost:3100/api/issues//comments?after=" # Expected: array of comments ``` ## Risks Low risk. The change only adds an early-return guard for values that are provably invalid UUIDs. The code path for valid UUIDs is unchanged. The existing behavior for anchor-not-found (returning `[]`) is preserved for invalid UUIDs, which is the correct semantic (cursor not found → no comments after it). ## Model Used Claude Sonnet 4.6 (`claude-sonnet-4-6`) via Paperclip CTO agent, tool use + code execution mode, 200K context window. ## 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 (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [ ] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip CTO Co-authored-by: Paperclip Co-authored-by: Andrew Aymeloglu --- server/src/__tests__/issues-service.test.ts | 38 +++++++++++++++++++++ server/src/services/issues.ts | 2 ++ 2 files changed, 40 insertions(+) diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index e90b0f1a4c..6baed72c3a 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -2451,6 +2451,44 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { expect(comments.map((comment) => comment.id)).toEqual([latestCommentId]); }); + it("returns no comments for an anchor cursor that is not a UUID", async () => { + const companyId = randomUUID(); + const issueId = randomUUID(); + const commentId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Malformed cursor issue", + status: "todo", + priority: "medium", + }); + + await db.insert(issueComments).values({ + id: commentId, + companyId, + issueId, + body: "Only comment", + createdAt: new Date("2026-03-26T10:00:00.000Z"), + updatedAt: new Date("2026-03-26T10:00:00.000Z"), + }); + + const comments = await svc.listComments(issueId, { + afterCommentId: commentId.slice(0, 8), + order: "asc", + limit: 50, + }); + + expect(comments).toEqual([]); + }); + it("lists user comments when derived run attribution scans a timestamp window", async () => { const companyId = randomUUID(); const agentId = randomUUID(); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 50ee3d6451..8d363ab17d 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -8796,6 +8796,8 @@ export function issueService(db: Db) { const conditions = [eq(issueComments.issueId, issueId)]; if (afterCommentId) { + // Guard: reject non-UUID cursors before hitting the DB to avoid Postgres type errors. + if (!isUuidLike(afterCommentId)) return []; const anchor = await db .select({ id: issueComments.id,