{"record":{"id":"3ed963f082d2fd86","repo":"paperclipai/paperclip","slug":"github-attachment-canonical-response-unavailable","errorCode":"github_attachment_canonical_response_unavailable","errorMessage":"github_attachment_canonical_response_unavailable","messagePattern":"github_attachment_canonical_response_unavailable","errorType":"error_code","errorClass":"GitHubAttachmentUnavailableError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-github-attachments.ts","lineNumber":546,"sourceCode":"    if (error instanceof GitHubAttachmentUnavailableError) return null;\n    throw error;\n  }\n}\n\nfunction resolveCanonicalAttachmentTargetOrThrow(\n  attachment: Attachment,\n  value: unknown,\n): URL {\n  const locator = handles.get(attachment);\n  const request = githubAttachmentCommentRequest(attachment);\n  if (\n    !locator ||\n    !request ||\n    !value ||\n    typeof value !== \"object\" ||\n    Array.isArray(value)\n  )\n    throw new GitHubAttachmentUnavailableError(\n      \"github_attachment_canonical_response_unavailable\",\n    );\n  const row = value as Record<string, unknown>;\n  const thread =\n    /^github:([^:]+):(?:(issue):)?([1-9][0-9]*)(?::rc:([1-9][0-9]*))?$/i.exec(\n      locator.sourceThreadId,\n    )!;\n  if (\n    String(row.id) !== locator.sourceMessageId ||\n    typeof row.url !== \"string\" ||\n    row.url.toLowerCase() !== request.url.toLowerCase()\n  )\n    throw new GitHubAttachmentUnavailableError(\n      \"github_attachment_canonical_source_mismatch\",\n    );\n  if (\n    typeof row.body !== \"string\" ||\n    row.body.length > 200_000 ||","sourceCodeStart":528,"sourceCodeEnd":564,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-github-attachments.ts#L528-L564","documentation":"resolveCanonicalAttachmentTargetOrThrow requires that the attachment be registered with a source locator and a canonical comment request, and that the caller pass a non-null, non-array JSON object as the canonical API response. If the internal handle/locator is missing (the attachment was not registered as a GitHub comment attachment) or the supplied value is not an object, it throws github_attachment_canonical_response_unavailable.","triggerScenarios":"resolveGitHubCommentAttachmentTarget is called with an attachment that has no entry in the internal `handles` map (never registered as a GitHub comment attachment); githubAttachmentCommentRequest(attachment) returns null; or the canonical API response `value` is null, undefined, an array, or a primitive instead of a JSON object.","commonSituations":"Passing the raw parsed JSON array of comments instead of the single comment object; calling resolution on a non-comment attachment type (e.g. a direct file attachment); calling before the fetch/registration step completed; a refactor changed the value passed from `response.json()` to something nested.","solutions":["Ensure the attachment went through the GitHub comment attachment registration path so a locator exists in `handles` before resolving.","Pass the single canonical comment JSON object (the parsed body of GET api.github.com/repos/{owner}/{repo}/issues/comments/{id}), not an array or wrapped envelope.","Only invoke resolveGitHubCommentAttachmentTarget for attachments created from GitHub comment sources; for other attachment kinds use the appropriate resolver (it returns null rather than throwing)."],"exampleFix":"// before\nconst target = resolveGitHubCommentAttachmentTarget(attachment, comments); // array\n// after\nconst comment = Array.isArray(comments) ? comments[0] : comments;\nconst target = resolveGitHubCommentAttachmentTarget(attachment, comment);","handlingStrategy":"type-guard","validationCode":"// Before resolving, confirm registration and shape\nconst req = githubAttachmentCommentRequest(attachment);\nif (!req) throw new Error('attachment is not a GitHub comment attachment');\nif (value == null || typeof value !== 'object' || Array.isArray(value))\n  throw new Error('canonical response must be a JSON object');","typeGuard":"function isCanonicalCommentObject(v: unknown): v is Record<string, unknown> {\n  return v !== null && typeof v === 'object' && !Array.isArray(v);\n}","tryCatchPattern":"const target = resolveGitHubCommentAttachmentTarget(attachment, value);\nif (target === null) {\n  // Includes github_attachment_canonical_response_unavailable; treat as unresolvable, not a crash\n  return renderFallback(attachment);\n}","preventionTips":["Only call resolveGitHubCommentAttachmentTarget on attachments that went through the GitHub comment registration path.","Always pass the parsed single-comment JSON object, never an array or an envelope like { comment: ... }.","Await the fetch/registration before resolution; do not resolve concurrently with registration.","When consuming response.json(), add a quick object-shape check before handing the value to the resolver."],"tags":["github","validation","missing-data","type-mismatch"],"backgroundTag":"unexpected-response-shape","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}