{"record":{"id":"14d00fbbbdd69409","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-cursor-invalid","errorCode":"paperclip_runner_chat_attachment_cursor_invalid","errorMessage":"paperclip_runner_chat_attachment_cursor_invalid","messagePattern":"paperclip_runner_chat_attachment_cursor_invalid","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":240,"sourceCode":"    typeof value === \"string\" &&\n    /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(\n      value,\n    )\n  );\n}\n\nfunction encodeListCursor(cursor: ListCursor): string {\n  return Buffer.from(JSON.stringify(cursor), \"utf8\").toString(\"base64url\");\n}\n\nfunction decodeListCursor(\n  value: unknown,\n  conversationId: string,\n  sourceCommentId: string | null,\n): ListCursor | null {\n  if (value === null || value === undefined) return null;\n  if (typeof value !== \"string\" || value.length === 0 || value.length > 1024) {\n    throw new Error(\"paperclip_runner_chat_attachment_cursor_invalid\");\n  }\n  try {\n    const parsed = record(\n      JSON.parse(Buffer.from(value, \"base64url\").toString(\"utf8\")),\n    );\n    const createdAt =\n      typeof parsed.createdAt === \"string\" ? parsed.createdAt : \"\";\n    const parsedDate = new Date(createdAt);\n    if (\n      parsed.schema !== \"paperclip.chat-attachment-list-cursor.v1\" ||\n      parsed.conversationId !== conversationId ||\n      (parsed.sourceCommentId ?? null) !== sourceCommentId ||\n      Number.isNaN(parsedDate.getTime()) ||\n      !isUuid(parsed.attachmentId) ||\n      !isUuid(parsed.sourceCommentIdTieBreak)\n    ) {\n      throw new Error(\"invalid\");\n    }","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L222-L258","documentation":"Thrown by decodeListCursor when the supplied pagination cursor is not a usable value at all: not a string, an empty string, or longer than 1024 characters. List cursors for chat-attachment reuse listings are base64url-encoded JSON blobs; anything failing this basic shape check is rejected before parsing. A null/undefined cursor is valid and means 'first page', only malformed values throw.","triggerScenarios":"Passing a cursor param that is a number, object, or array instead of a string; passing an empty string \"\"; passing a cursor string exceeding 1024 chars (e.g. a client echoing back a bloated or corrupted cursor).","commonSituations":"Client sends cursor as a query param and it arrives as something other than a string after parsing; truncated cursor values from URL length limits; a client constructing cursors by hand instead of using the previously returned next-cursor; a client sending \"\" meaning 'first page' instead of omitting the parameter.","solutions":["Omit the cursor parameter entirely (or send null) to request the first page — only send back the exact cursor string the API returned.","Validate the cursor is a non-empty string of at most 1024 characters before sending it.","Regenerate the cursor by re-fetching page 1 and paginating forward with returned cursors.","If cursors were persisted, check that the stored value was not truncated or re-encoded (base64url is case-sensitive; URL-decoding/encoding damage is common)."],"exampleFix":"// before\nconst items = await listAttachmentReuse({ cursor: searchParams.cursor ?? \"\" });\n\n// after\nconst cursor = typeof searchParams.cursor === \"string\" && searchParams.cursor.length > 0 && searchParams.cursor.length <= 1024\n  ? searchParams.cursor\n  : undefined;\nconst items = await listAttachmentReuse({ cursor });","handlingStrategy":"validation","validationCode":"function cursorIsSendable(cursor: unknown): boolean {\n  return typeof cursor === \"string\" && cursor.length > 0 && cursor.length <= 1024;\n}","typeGuard":"function isValidCursorString(value: unknown): value is string {\n  return typeof value === \"string\" && value.length > 0 && value.length <= 1024;\n}","tryCatchPattern":"try {\n  page = await listAttachmentReuse({ cursor });\n} catch (err) {\n  if (err instanceof Error && err.message === \"paperclip_runner_chat_attachment_cursor_invalid\") {\n    page = await listAttachmentReuse({}); // restart from first page\n  } else {\n    throw err;\n  }\n}","preventionTips":["Always echo cursors back verbatim from API responses; never build them by hand.","Treat a missing/empty cursor as 'first page' — omit the parameter rather than sending \"\".","Bound persisted cursor storage to the 1024-char limit and validate before use.","Don't round-trip cursors through transformations (URL decode, base64 normalize) that alter bytes."],"tags":["pagination","cursor","validation","attachments"],"backgroundTag":"invalid-argument-format","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}