{"record":{"id":"41e25a67ad907089","repo":"paperclipai/paperclip","slug":"github-attachment-invalid-response","errorCode":"github_attachment_invalid_response","errorMessage":"github_attachment_invalid_response","messagePattern":"github_attachment_invalid_response","errorType":"error_code","errorClass":"GitHubAttachmentUnavailableError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-github-attachments.ts","lineNumber":817,"sourceCode":"          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\",\n        );\n      const name = attachment.name?.startsWith(\"github-attachment-\")\n        ? `${attachment.name}${MIME_EXTENSIONS[mimeType] ?? \"\"}`\n        : attachment.name;\n      return {\n        type: mimeType.startsWith(\"image/\")\n          ? \"image\"","sourceCodeStart":799,"sourceCodeEnd":835,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-github-attachments.ts#L799-L835","documentation":"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.","triggerScenarios":"Response declares Content-Length N without Content-Encoding, and the decoded stream delivers size !== N (truncated transfer, connection reset mid-body, lying proxy).","commonSituations":"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.","solutions":["Retry the download — transient truncation often resolves on a fresh request","Remove/fix intermediaries (proxies, compression middleware) that set inconsistent Content-Length/Content-Encoding pairs","If compressing, ensure Content-Length is omitted or matches the compressed bytes with Content-Encoding set","Validate the URL serves the asset directly without rewriting"],"exampleFix":"// before: test double with mismatched headers\nnew Response(buffer.slice(0, 10), { headers: { \"content-length\": String(buffer.length) } });\n// after: consistent headers and body\nnew Response(buffer, { headers: { \"content-length\": String(buffer.length), \"content-type\": \"image/png\" } });","handlingStrategy":"retry","validationCode":"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","typeGuard":"null","tryCatchPattern":"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; } }","preventionTips":["Fix intermediaries that set Content-Length inconsistent with Content-Encoding","Prefer proxies that pass GitHub responses through unmodified","Retry once on transient truncation before surfacing the error","Return accurate Content-Length in test doubles"],"tags":["github","attachments","content-length","http"],"backgroundTag":"unexpected-response-shape","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}