paperclipai/paperclip · error · GitHubAttachmentUnavailableError

github_attachment_empty

github_attachment_empty

Error message

github_attachment_empty

What it means

After fully reading the attachment stream, prepareGitHubPublicAttachment throws GitHubAttachmentUnavailableError with code "github_attachment_empty" at chat-github-attachments.ts:809-810 when zero bytes were received (size === 0). An empty body can never be a valid image/file attachment, so the download is rejected rather than returning a 0-byte attachment.

Source

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

      try {
        for (;;) {
          signal.throwIfAborted();
          const next = await reader.read();
          signal.throwIfAborted();
          if (next.done) break;
          size += next.value.byteLength;
          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",

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the attachment URL resolves to real content (open it in a browser; it should download, not be 0 bytes)
  2. Re-upload or regenerate the attachment so a fresh URL with actual bytes exists
  3. Check proxies/CDNs between the server and GitHub for body-stripping behavior
  4. If the source is a GitHub comment asset, ensure the canonical resolution found the correct asset URL

Example fix

// before: stub returns 200 with no body
fetchMock.mockResolvedValue(new Response(null, { status: 200 }));
// after: stub returns actual image bytes
fetchMock.mockResolvedValue(new Response(pngBytes, { status: 200, headers: { "content-type": "image/png", "content-length": String(pngBytes.length) } }));
Defensive patterns

Strategy: try-catch

Validate before calling

const probe = await fetch(url, { method: "GET", headers: { range: "bytes=0-15" } }); if (probe.status === 200 && (await probe.arrayBuffer()).byteLength === 0) return skip("empty body");

Type guard

null

Try / catch

try { return await prepareGitHubPublicAttachment(attachment, signal); } catch (e) { if (e?.code === "github_attachment_empty") { return unavailableAttachmentCard(attachment); } throw e; }

Prevention

When it happens

Trigger: The remote server returns HTTP 200 with a zero-length body, or the stream ends immediately with no chunks, so the size counter stays 0 after the read loop.

Common situations: GitHub media endpoints returning 200 with empty body for deleted/expired assets; misbehaving proxies or CDN edge nodes; aborted-but-not-signalled upstream uploads; testing against a stub server that returns 200 without a body.

Related errors


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