{"record":{"id":"78ef6a720f6def59","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-read-integrity-mismatch","errorCode":"paperclip_runner_chat_attachment_read_integrity_mismatch","errorMessage":"paperclip_runner_chat_attachment_read_integrity_mismatch","messagePattern":"paperclip_runner_chat_attachment_read_integrity_mismatch","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-read.ts","lineNumber":251,"sourceCode":"        for await (const chunk of value.stream) {\n          const bytes = Buffer.from(chunk);\n          length += bytes.length;\n          if (length > source.byteSize || length > MAX_ATTACHMENT_BYTES)\n            throw new Error(\n              \"paperclip_runner_chat_attachment_read_size_mismatch\",\n            );\n          chunks.push(bytes);\n        }\n      } finally {\n        value.stream.destroy();\n      }\n      const body = Buffer.concat(chunks);\n      if (\n        length !== source.byteSize ||\n        createHash(\"sha256\").update(body).digest(\"hex\") !==\n          source.sha256.toLowerCase()\n      )\n        throw new Error(\n          \"paperclip_runner_chat_attachment_read_integrity_mismatch\",\n        );\n      return body;\n    })();\n    return Promise.race([read, aborted]);\n  }\n\n  close(): Promise<void> {\n    if (this.#closing) return this.#closing;\n    this.#closed = true;\n    this.#abort.abort();\n    this.#closing = (async () => {\n      await Promise.allSettled([...this.#pending]);\n      const results = await Promise.allSettled(\n        this.#cleanups.map((cleanup) => cleanup()),\n      );\n      if (results.some((result) => result.status === \"rejected\"))\n        throw new Error(\"paperclip_runner_chat_attachment_read_cleanup_failed\");","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-read.ts#L233-L269","documentation":"After fully reading the stream, #bytes verifies the total length equals source.byteSize and that the SHA-256 of the body matches source.sha256. Any mismatch means the bytes delivered by storage do not correspond to the authorized attachment, so the content is discarded and this error is thrown. This is the final content-integrity gate before staging.","triggerScenarios":"Storage returns truncated or corrupted bytes (short read, interrupted transfer); object content was mutated without updating metadata; hash-case mismatch after a buggy writer stored an uppercase hash; partial upload committed with stale sha256 metadata.","commonSituations":"Flaky storage backends dropping stream tails; bit-rot or manual object replacement in the bucket; re-encryption pipelines changing bytes but not metadata.","solutions":["Re-upload the attachment so bytes and sha256/byteSize metadata agree, then retry the read.","Check the storage backend for truncation/corruption (multipart completeness, checksums).","Fix any writer that mutates object bytes without updating the recorded sha256.","Retry once to rule out a transient truncated transfer."],"exampleFix":"// before\nawait replaceObject(key, newBytes); // metadata sha256 now stale\n// after\nawait replaceObject(key, newBytes, { sha256: sha256(newBytes), byteSize: newBytes.byteLength });","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isIntegrityError(e: unknown): boolean {\n  return e instanceof Error && e.message === \"paperclip_runner_chat_attachment_read_integrity_mismatch\";\n}","tryCatchPattern":"try {\n  return await scope.read(input);\n} catch (e) {\n  if (isIntegrityError(e)) {\n    return { status: \"integrity_failure\" }; // re-upload the attachment; never stage unverified bytes\n  }\n  throw e;\n}","preventionTips":["Store sha256 alongside every uploaded object and verify after upload.","Use checksum-validated storage uploads (Content-MD5 / x-amz-checksum).","Never mutate object bytes without updating sha256/byteSize metadata.","Retry once for transient truncation, then flag the object as corrupt."],"tags":["integrity","checksum","sha256","storage"],"backgroundTag":"checksum-mismatch","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}