paperclipai/paperclip · error · GitHubAttachmentUnavailableError

github_attachment_canonical_body_mismatch

github_attachment_canonical_body_mismatch

Error message

github_attachment_canonical_body_mismatch

What it means

resolveCanonicalAttachmentTargetOrThrow verifies that the canonical comment body is the exact text the attachment was created from: row.body must be a string of at most 200,000 characters and its SHA-256 hex digest must equal locator.sourceBodySha256. If the body is missing, oversized, or its hash differs from the pinned hash, it throws github_attachment_canonical_body_mismatch, because the HTML rendering would no longer be evidence for the unchanged source the attachment points at.

Source

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

  const thread =
    /^github:([^:]+):(?:(issue):)?([1-9][0-9]*)(?::rc:([1-9][0-9]*))?$/i.exec(
      locator.sourceThreadId,
    )!;
  if (
    String(row.id) !== locator.sourceMessageId ||
    typeof row.url !== "string" ||
    row.url.toLowerCase() !== request.url.toLowerCase()
  )
    throw new GitHubAttachmentUnavailableError(
      "github_attachment_canonical_source_mismatch",
    );
  if (
    typeof row.body !== "string" ||
    row.body.length > 200_000 ||
    createHash("sha256").update(row.body).digest("hex") !==
      locator.sourceBodySha256
  )
    throw new GitHubAttachmentUnavailableError(
      "github_attachment_canonical_body_mismatch",
    );
  if (typeof row.body_html !== "string" || row.body_html.length > 600_000)
    throw new GitHubAttachmentUnavailableError(
      "github_attachment_canonical_html_unavailable",
    );
  if (thread[4]) {
    if (
      row.pull_request_url !==
        `https://api.github.com/repos/${thread[1]}/pulls/${thread[3]}` ||
      String(row.in_reply_to_id ?? row.id) !== thread[4]
    )
      throw new GitHubAttachmentUnavailableError(
        "github_attachment_canonical_source_mismatch",
      );
  } else if (
    row.issue_url !==
    `https://api.github.com/repos/${thread[1]}/issues/${thread[3]}`

View on GitHub (pinned to 01ad858492)

Solutions

  1. Re-attach the image/file from the current version of the comment so sourceBodySha256 is recomputed against the edited body.
  2. Restore the comment body to its original content if the edit was unintentional (GitHub keeps edit history for comments).
  3. If bodies are legitimately large, confirm they stay under the 200,000-character limit or adjust the limit in your deployment.
  4. Check that no middleware rewrites the body field (e.g. HTML-escaping or whitespace normalization) between fetch and verification; compare hashes locally with `sha256sum` against the stored value.

Example fix

// before: verifying against a stale hash after the comment was edited
const ok = sha256(row.body) === locator.sourceBodySha256; // fails silently every time
// after: refresh the locator when the body legitimately changed
const fresh = await captureAttachmentLocator(attachment.url); // recompute sha256
await updateAttachmentLocator(attachment.id, fresh);
Defensive patterns

Strategy: validation

Validate before calling

// Pre-verify the body hash before resolution
import { createHash } from 'node:crypto';
const digest = createHash('sha256').update(String(row.body ?? '')).digest('hex');
if (typeof row.body !== 'string' || row.body.length > 200_000 || digest !== locator.sourceBodySha256)
  throw new Error('comment body changed or exceeds limit');

Type guard

function bodyIntact(row: Record<string, unknown>, expectedSha256: string): boolean {
  return typeof row.body === 'string' &&
    row.body.length <= 200_000 &&
    createHash('sha256').update(row.body).digest('hex') === expectedSha256;
}

Try / catch

const target = resolveGitHubCommentAttachmentTarget(attachment, value);
if (target === null) {
  // body mismatch mapped to null: the source changed — require re-attachment, do not retry
  notifyUserToReattach(attachment);
  return null;
}

Prevention

When it happens

Trigger: The GitHub comment body was edited after the attachment locator recorded sourceBodySha256; row.body is absent or not a string in the API response; the comment body exceeds 200,000 characters; the body hash computation differs (e.g. body normalized/transformed between capture and verification).

Common situations: A user edited or reflowed the comment after the file was attached; re-resolution happens against an older/newer revision of the comment; a proxy or API version changes whitespace/encoding in body so the SHA-256 no longer matches; extremely long comments exceed the 200k cap.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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