paperclipai/paperclip · error · GitHubAttachmentUnavailableError
github_attachment_canonical_target_denied
github_attachment_canonical_target_denied
Error message
github_attachment_canonical_target_denied
What it means
GitHubAttachmentUnavailableError with code github_attachment_canonical_target_denied is thrown when the candidate signed image URL extracted from a matching anchor is rejected: either the img src does not resolve to a valid signed private-user-images URL (single 'jwt' query param), or the anchor's href form fails the consistency check (href must equal the locator URL, or be the same signed URL as the img src). This prevents choosing an unverifiable or mixed rendering target.
Source
Thrown at server/src/services/chat-github-attachments.ts:644
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) =>
image.getAttribute("src") === locator.url ||
sameAssetImage(image.getAttribute("src")!),
);
if (candidates.length > 1 || sameAssetImages.length > 1)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_mapping_ambiguous",
);
if (candidates.length === 1 && sameAssetImages.length === 1)
return candidates[0]!;
if (
[...fragment.querySelectorAll("img[src]")].some((image) =>
signedImage(image.getAttribute("src")!),View on GitHub (pinned to 01ad858492)
Solutions
- Re-fetch the comment so body, body_html, and signed jwt URLs are mutually consistent (a fresh render pairs anchors with current jwts).
- Ensure the anchor href is either the original locator URL or byte-identical to the signed img src.
- Remove any proxy rewriting of private-user-images URLs (extra params break the single-jwt contract).
- Verify the jwt in the signed URL has the standard header.payload.signature three-segment shape.
Example fix
// before (proxy added params) https://private-user-images.../1-abc.png?jwt=eyJ...&x-id=GetObject // after (unproxied GitHub render) https://private-user-images.../1-abc.png?jwt=eyJ...
Defensive patterns
Strategy: try-catch
Validate before calling
const isCleanSignedUrl = (u: string) => {
try { const p = new URL(u); return p.hostname === 'private-user-images.githubusercontent.com' && [...p.searchParams.keys()].join(',') === 'jwt'; } catch { return false; }
};
if (!isCleanSignedUrl(imgSrc)) console.warn('signed URL altered; resolution will be denied'); Type guard
const hasSingleJwt = (u: string): boolean => {
try { const url = new URL(u); return /^[a-z0-9_-]+\.[a-z0-9_-]+\.[a-z0-9_-]+$/i.test(url.searchParams.get('jwt') ?? ''); } catch { return false; }
}; Try / catch
const target = resolveGitHubCommentAttachmentTarget(attachment, row);
if (target === null) {
// denied: refetch the comment to get a consistent render, then retry once
const freshRow = await refetchComment(row.url);
retryResolve(attachment, freshRow);
} Prevention
- Re-fetch the whole comment rather than mixing cached HTML with fresh signed URLs.
- Disable proxies/CDN rewrites that add query params to private-user-images URLs.
- Never hot-paste signed URLs into comment markdown.
- Verify jwt three-segment shape before relying on signed links.
When it happens
Trigger: A matching anchor exists but its img src is an unsigned /assets/ URL, a signed URL with extra/missing query parameters, or a jwt that fails the three-segment format check; or the anchor href is a signed URL different from the img src and not the original locator URL.
Common situations: GitHub rendering with expired/refreshed jwt query params mixed with cached HTML; comments whose markdown hot-links the signed URL directly with altered params; proxy/CDN rewrites injecting extra query parameters; fixtures using fake signed URLs.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- github_attachment_source_mismatch
- github_attachment_canonical_html_unavailable
- github_attachment_canonical_file_unsupported
- github_attachment_canonical_image_count_invalid
- github_attachment_canonical_mapping_ambiguous
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/d36960e3691d07a6.
Report an issue: GitHub.