paperclipai/paperclip · error · GitHubAttachmentUnavailableError

github_attachment_canonical_image_count_invalid

github_attachment_canonical_image_count_invalid

Error message

github_attachment_canonical_image_count_invalid

What it means

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.

Source

Thrown at server/src/services/chat-github-attachments.ts:632

        target.searchParams.get("jwt") ?? "",
      ) &&
      !sourceBody.includes(src)
      ? target
      : null;
  };
  const fragment = JSDOM.fragment(row.body_html);
  const candidates: URL[] = [];
  for (const anchor of fragment.querySelectorAll("a[href]")) {
    const href = anchor.getAttribute("href")!;
    const images = anchor.querySelectorAll("img[src]");
    if (
      href !== locator.url &&
      !sameAssetImage(href) &&
      ![...images].some((image) => sameAssetImage(image.getAttribute("src")!))
    )
      continue;
    if (images.length !== 1)
      throw new GitHubAttachmentUnavailableError(
        "github_attachment_canonical_image_count_invalid",
      );
    const src = images[0]!.getAttribute("src")!;
    const target = signedImage(src);
    // The second form was observed in the exact App-rendered live comment.
    // Both the original-anchor and signed-anchor forms enter one candidate set
    // so duplicated or mixed renderings cannot silently choose a target.
    if (
      !target ||
      (href !== locator.url && (href !== src || !signedImage(href)))
    )
      throw new GitHubAttachmentUnavailableError(
        "github_attachment_canonical_target_denied",
      );
    candidates.push(target);
  }
  const sameAssetImages = [...fragment.querySelectorAll("img[src]")].filter(
    (image) =>

View on GitHub (pinned to 01ad858492)

Solutions

  1. Fix the comment on GitHub so each asset anchor wraps exactly one image (split multi-image links into separate links).
  2. Re-fetch the comment; the edit may have been transient — the body must also match the recorded SHA-256.
  3. If you control fixtures, mirror GitHub's real rendering: one <a href=...><img src=...></a> per image.

Example fix

// before (multiple images in one anchor)
<a href="https://github.com/user-attachments/assets/abc"><img src="i1"><img src="i2"></a>
// after
<a href="https://github.com/user-attachments/assets/abc"><img src="i1"></a>
Defensive patterns

Strategy: try-catch

Validate before calling

// best-effort pre-check on raw markdown: one link per image
const anchorCount = (body.match(/\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\]/g) || []).length;
const imageCount = (body.match(/!\[[^\]]*\]\([^)]*\)/g) || []).length;
if (anchorCount !== imageCount) console.warn('multi-image anchors likely; canonical resolution may fail');

Try / catch

const target = resolveGitHubCommentAttachmentTarget(attachment, row);
if (target === null) {
  // GitHubAttachmentUnavailableError was swallowed; fall back to refusing the attachment
  denyAttachment(attachment, 'unresolvable-anchor');
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/a96268f2d4eaca4d. Report an issue: GitHub.