paperclipai/paperclip · error · GitHubAttachmentUnavailableError

github_attachment_invalid_response

github_attachment_invalid_response

Error message

github_attachment_invalid_response

What it means

After streaming, the service compares the received byte count against the Content-Length header at chat-github-attachments.ts:811-819 and throws GitHubAttachmentUnavailableError with code "github_attachment_invalid_response" on mismatch — but only when no Content-Encoding header is present, because Content-Length then describes compressed bytes, not the decoded size the reader observed. This detects truncated or tampered downloads.

Source

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

          if (size > MAX_ATTACHMENT_BYTES)
            throw new GitHubAttachmentUnavailableError(
              "github_attachment_too_large",
            );
          chunks.push(Buffer.from(next.value));
        }
      } finally {
        signal.removeEventListener("abort", cancel);
        await reader.cancel().catch(() => undefined);
      }
      if (!size)
        throw new GitHubAttachmentUnavailableError("github_attachment_empty");
      // Content-Length describes compressed bytes when Content-Encoding is present.
      if (
        declared &&
        !response.headers.get("content-encoding") &&
        size !== Number(declared)
      )
        throw new GitHubAttachmentUnavailableError(
          "github_attachment_invalid_response",
        );
      const body = Buffer.concat(chunks, size);
      if (
        !imageSignatureMatches(body, mimeType) ||
        /^\s*(?:<!doctype\s+html|<html\b)/i.test(
          body.subarray(0, 512).toString("utf8"),
        )
      )
        throw new GitHubAttachmentUnavailableError(
          "github_attachment_invalid_response",
        );
      const name = attachment.name?.startsWith("github-attachment-")
        ? `${attachment.name}${MIME_EXTENSIONS[mimeType] ?? ""}`
        : attachment.name;
      return {
        type: mimeType.startsWith("image/")
          ? "image"

View on GitHub (pinned to 01ad858492)

Solutions

  1. Retry the download — transient truncation often resolves on a fresh request
  2. Remove/fix intermediaries (proxies, compression middleware) that set inconsistent Content-Length/Content-Encoding pairs
  3. If compressing, ensure Content-Length is omitted or matches the compressed bytes with Content-Encoding set
  4. Validate the URL serves the asset directly without rewriting

Example fix

// before: test double with mismatched headers
new Response(buffer.slice(0, 10), { headers: { "content-length": String(buffer.length) } });
// after: consistent headers and body
new Response(buffer, { headers: { "content-length": String(buffer.length), "content-type": "image/png" } });
Defensive patterns

Strategy: retry

Validate before calling

const head = await fetch(url, { method: "HEAD" }); const declared = head.headers.get("content-length"); const enc = head.headers.get("content-encoding"); if (declared && !enc) expectedSize = Number(declared); // compare after download

Type guard

null

Try / catch

for (let attempt = 0; attempt < 2; attempt++) { try { return await prepareGitHubPublicAttachment(attachment, signal); } catch (e) { if (e?.code === "github_attachment_invalid_response" && attempt === 0) continue; throw e; } }

Prevention

When it happens

Trigger: Response declares Content-Length N without Content-Encoding, and the decoded stream delivers size !== N (truncated transfer, connection reset mid-body, lying proxy).

Common situations: Flaky networks or proxies truncating bodies; misconfigured compression middleware that sets Content-Length of the uncompressed size while also gzipping; custom test doubles with inconsistent headers/body lengths; HTTP/2 flow-control early termination.

Related errors


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