{"record":{"id":"a96268f2d4eaca4d","repo":"paperclipai/paperclip","slug":"github-attachment-canonical-image-count-invalid","errorCode":"github_attachment_canonical_image_count_invalid","errorMessage":"github_attachment_canonical_image_count_invalid","messagePattern":"github_attachment_canonical_image_count_invalid","errorType":"error_code","errorClass":"GitHubAttachmentUnavailableError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-github-attachments.ts","lineNumber":632,"sourceCode":"        target.searchParams.get(\"jwt\") ?? \"\",\n      ) &&\n      !sourceBody.includes(src)\n      ? target\n      : null;\n  };\n  const fragment = JSDOM.fragment(row.body_html);\n  const candidates: URL[] = [];\n  for (const anchor of fragment.querySelectorAll(\"a[href]\")) {\n    const href = anchor.getAttribute(\"href\")!;\n    const images = anchor.querySelectorAll(\"img[src]\");\n    if (\n      href !== locator.url &&\n      !sameAssetImage(href) &&\n      ![...images].some((image) => sameAssetImage(image.getAttribute(\"src\")!))\n    )\n      continue;\n    if (images.length !== 1)\n      throw new GitHubAttachmentUnavailableError(\n        \"github_attachment_canonical_image_count_invalid\",\n      );\n    const src = images[0]!.getAttribute(\"src\")!;\n    const target = signedImage(src);\n    // The second form was observed in the exact App-rendered live comment.\n    // Both the original-anchor and signed-anchor forms enter one candidate set\n    // so duplicated or mixed renderings cannot silently choose a target.\n    if (\n      !target ||\n      (href !== locator.url && (href !== src || !signedImage(href)))\n    )\n      throw new GitHubAttachmentUnavailableError(\n        \"github_attachment_canonical_target_denied\",\n      );\n    candidates.push(target);\n  }\n  const sameAssetImages = [...fragment.querySelectorAll(\"img[src]\")].filter(\n    (image) =>","sourceCodeStart":614,"sourceCodeEnd":650,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-github-attachments.ts#L614-L650","documentation":"GitHubAttachmentUnavailableError with code github_attachment_canonical_image_count_invalid is thrown when an anchor in the rendered HTML that matches the source asset (by href or embedded img src) does not contain exactly one img element. The resolver's contract is GitHub's image-anchor mapping: each matching anchor must wrap exactly one image, otherwise the mapping is not trustworthy.","triggerScenarios":"The rendered body_html contains a matching <a> whose href equals the locator URL or a same-asset signed URL, but the anchor wraps zero or multiple <img> tags (e.g. nested/stacked thumbnails, edited markdown with multiple images in one link).","commonSituations":"Users editing a comment to link one asset URL around several images; GitHub rendering changes altering anchor structure; hand-written HTML/markdown like [![a](img1)](url) plus a second image inside the same anchor; fixture HTML that doesn't match GitHub's real rendering.","solutions":["Fix the comment on GitHub so each asset anchor wraps exactly one image (split multi-image links into separate links).","Re-fetch the comment; the edit may have been transient — the body must also match the recorded SHA-256.","If you control fixtures, mirror GitHub's real rendering: one <a href=...><img src=...></a> per image."],"exampleFix":"// before (multiple images in one anchor)\n<a href=\"https://github.com/user-attachments/assets/abc\"><img src=\"i1\"><img src=\"i2\"></a>\n// after\n<a href=\"https://github.com/user-attachments/assets/abc\"><img src=\"i1\"></a>","handlingStrategy":"try-catch","validationCode":"// best-effort pre-check on raw markdown: one link per image\nconst anchorCount = (body.match(/\\[!\\[[^\\]]*\\]\\([^)]*\\)\\]\\([^)]*\\)\\]/g) || []).length;\nconst imageCount = (body.match(/!\\[[^\\]]*\\]\\([^)]*\\)/g) || []).length;\nif (anchorCount !== imageCount) console.warn('multi-image anchors likely; canonical resolution may fail');","typeGuard":null,"tryCatchPattern":"const target = resolveGitHubCommentAttachmentTarget(attachment, row);\nif (target === null) {\n  // GitHubAttachmentUnavailableError was swallowed; fall back to refusing the attachment\n  denyAttachment(attachment, 'unresolvable-anchor');\n}","preventionTips":["Keep one image per markdown link in GitHub comments.","Avoid nested/multi-image anchors when editing comments.","After editing a comment, re-record the body SHA-256 in the locator.","Use GitHub's real rendered HTML (not hand-made fixtures) for tests."],"tags":["github","attachments","html-parsing","images"],"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-14T00:17:10.932Z"}