{"record":{"id":"6fbeebb1fd7b5f55","repo":"paperclipai/paperclip","slug":"github-attachment-empty","errorCode":"github_attachment_empty","errorMessage":"github_attachment_empty","messagePattern":"github_attachment_empty","errorType":"error_code","errorClass":"GitHubAttachmentUnavailableError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-github-attachments.ts","lineNumber":810,"sourceCode":"      try {\n        for (;;) {\n          signal.throwIfAborted();\n          const next = await reader.read();\n          signal.throwIfAborted();\n          if (next.done) break;\n          size += next.value.byteLength;\n          if (size > MAX_ATTACHMENT_BYTES)\n            throw new GitHubAttachmentUnavailableError(\n              \"github_attachment_too_large\",\n            );\n          chunks.push(Buffer.from(next.value));\n        }\n      } finally {\n        signal.removeEventListener(\"abort\", cancel);\n        await reader.cancel().catch(() => undefined);\n      }\n      if (!size)\n        throw new GitHubAttachmentUnavailableError(\"github_attachment_empty\");\n      // Content-Length describes compressed bytes when Content-Encoding is present.\n      if (\n        declared &&\n        !response.headers.get(\"content-encoding\") &&\n        size !== Number(declared)\n      )\n        throw new GitHubAttachmentUnavailableError(\n          \"github_attachment_invalid_response\",\n        );\n      const body = Buffer.concat(chunks, size);\n      if (\n        !imageSignatureMatches(body, mimeType) ||\n        /^\\s*(?:<!doctype\\s+html|<html\\b)/i.test(\n          body.subarray(0, 512).toString(\"utf8\"),\n        )\n      )\n        throw new GitHubAttachmentUnavailableError(\n          \"github_attachment_invalid_response\",","sourceCodeStart":792,"sourceCodeEnd":828,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-github-attachments.ts#L792-L828","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the attachment URL resolves to real content (open it in a browser; it should download, not be 0 bytes)","Re-upload or regenerate the attachment so a fresh URL with actual bytes exists","Check proxies/CDNs between the server and GitHub for body-stripping behavior","If the source is a GitHub comment asset, ensure the canonical resolution found the correct asset URL"],"exampleFix":"// before: stub returns 200 with no body\nfetchMock.mockResolvedValue(new Response(null, { status: 200 }));\n// after: stub returns actual image bytes\nfetchMock.mockResolvedValue(new Response(pngBytes, { status: 200, headers: { \"content-type\": \"image/png\", \"content-length\": String(pngBytes.length) } }));","handlingStrategy":"try-catch","validationCode":"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\");","typeGuard":"null","tryCatchPattern":"try { return await prepareGitHubPublicAttachment(attachment, signal); } catch (e) { if (e?.code === \"github_attachment_empty\") { return unavailableAttachmentCard(attachment); } throw e; }","preventionTips":["Validate attachment URLs resolve to real bytes before embedding them in chat","Re-upload assets whose source upload was interrupted","Watch for CDN/proxy misconfigurations that strip response bodies","Ensure test doubles return non-empty bodies with 200"],"tags":["github","attachments","empty-body","http"],"backgroundTag":"empty-response-body","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}