fix: guard listComments against non-UUID afterCommentId to prevent 500 errors (#8695)
## 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=<commentId>` 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=<value>` 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/<issue-id>/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/<any-valid-issue-id>/comments?after=670427ab" # Expected: [] # Pass a valid full UUID that doesn't exist — should also return [] curl -s "http://localhost:3100/api/issues/<any-valid-issue-id>/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/<any-valid-issue-id>/comments?after=<real-comment-uuid>" # 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 <cto@paperclip.ai> Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Andrew Aymeloglu <aaymeloglu@gmail.com>
This commit is contained in:
parent
333abdd2c2
commit
505e7b40fc
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue