paperclipai/paperclip · error · GitHubAttachmentUnavailableError
github_attachment_source_mismatch
github_attachment_source_mismatch
Error message
github_attachment_source_mismatch
What it means
githubAttachmentCommentFetch returns a hardened fetch wrapper that only permits the single canonical GitHub API comment request derived from the attachment locator. If the wrapped fetch is invoked with anything other than the expected URL string, a non-GET method, or after the expected request record fails its own shape validation, it throws GitHubAttachmentUnavailableError with code github_attachment_source_mismatch. This is a deliberate SSRF/provenance guard: the authenticated client must never be pointed at a caller-chosen origin.
Source
Thrown at server/src/services/chat-github-attachments.ts:435
: "application/vnd.github.full+json",
};
}
const MAX_COMMENT_RESPONSE_BYTES = 1_048_576;
/** Octokit's authenticated request may go only to this one fixed API route. */
export function githubAttachmentCommentFetch(
expected: GitHubAttachmentCommentRequest,
signal: AbortSignal,
): typeof fetch {
return async (input, init) => {
if (
!isGitHubAttachmentCommentRequest(expected) ||
typeof input !== "string" ||
input !== expected.url ||
init?.method !== "GET"
)
throw new GitHubAttachmentUnavailableError(
"github_attachment_source_mismatch",
);
signal.throwIfAborted();
const headers = new Headers(init.headers);
if (headers.get("accept") !== expected.accept || headers.has("cookie"))
throw new GitHubAttachmentUnavailableError(
"github_attachment_source_mismatch",
);
const response = await guardedRemoteHttpFetch(
expected.url,
{
...init,
method: "GET",
headers,
credentials: "omit",
redirect: "manual",
signal,
},View on GitHub (pinned to 01ad858492)
Solutions
- Call the wrapper only with expected.url verbatim and method "GET" — no URL rewriting, no added query parameters
- Regenerate the expected request via githubAttachmentCommentRequest(attachment) so the locator has version 2 and sourceBodySha256 set
- Ensure redirects are handled by the wrapper itself (it uses redirect: "manual") and no upstream code follows them through this fetch
- If you need a different endpoint, use a normal fetch — this wrapper is intentionally restricted to the one canonical comment route
Example fix
// before: caller rewrote the URL with query params
await scopedFetch(`${expected.url}?per_page=1`, { method: "GET", headers });
// after: use the expected URL exactly
await scopedFetch(expected.url, { method: "GET", headers }); Defensive patterns
Strategy: type-guard
Validate before calling
const expected = githubAttachmentCommentRequest(attachment);
if (!expected) throw new Error("attachment locator is not a v2 github comment attachment");
// pass expected.url verbatim to the wrapped fetch, method GET only Type guard
function isCanonicalCommentFetchCall(expected: GitHubAttachmentCommentRequest, input: RequestInfo | URL, init?: RequestInit): boolean {
return typeof input === "string" && input === expected.url && init?.method === "GET";
} Try / catch
try {
const res = await scopedFetch(expected.url, { method: "GET", headers: { accept: expected.accept }, signal });
} catch (err) {
if (err instanceof GitHubAttachmentUnavailableError && err.code === "github_attachment_source_mismatch") {
throw new Error("request was rewritten; only the exact canonical comment URL with GET is allowed");
} else throw err;
} Prevention
- Never append query parameters, change hosts, or follow redirects through this restricted fetch
- Always build the expected request object with githubAttachmentCommentRequest and reuse it for both URL and Accept
- Keep the attachment locator at version 2 with sourceBodySha256 set so isGitHubAttachmentCommentRequest passes
When it happens
Trigger: The returned fetch is called with input !== expected.url, a non-string input, init.method !== "GET", or isGitHubAttachmentCommentRequest(expected) is false (locator missing version 2 or sourceBodySha256).
Common situations: Octokit or an internal HTTP client rewrites the URL (adds query params, different host like github.com instead of api.github.com) before calling fetch; a redirect or retry logic issues a non-GET method; code reuses this wrapper for a different attachment whose locator lacks the required fields; proxy middleware modifies the request URL.
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_canonical_target_denied
- GitHub webhook configuration could not be confirmed. Reconne
- Teams destination contains an untrusted service URL
- devUiUrl must use http or https protocol
- devUiUrl must target localhost
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/38a6d2d98c0174b5.
Report an issue: GitHub.