{"record":{"id":"676d3b487d77d4b0","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-storage-mismatch","errorCode":"paperclip_runner_chat_attachment_storage_mismatch","errorMessage":"paperclip_runner_chat_attachment_storage_mismatch","messagePattern":"paperclip_runner_chat_attachment_storage_mismatch","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":1450,"sourceCode":"  );\n  const stored = await putStorageObjectWithin(\n    storage,\n    {\n      companyId: input.binding.companyId,\n      namespace: `issues/${input.binding.issueId}`,\n      originalFilename: input.source.filename,\n      contentType: input.source.contentType,\n      body,\n    },\n    storageTimeoutMs,\n  );\n  try {\n    if (\n      stored.byteSize !== body.length ||\n      stored.sha256.toLowerCase() !== input.source.sha256.toLowerCase() ||\n      stored.contentType !== input.source.contentType\n    ) {\n      throw new Error(\"paperclip_runner_chat_attachment_storage_mismatch\");\n    }\n    const attachment = await issueService(input.db).createAttachment({\n      issueId: input.binding.issueId,\n      provider: stored.provider,\n      objectKey: stored.objectKey,\n      contentType: stored.contentType,\n      byteSize: stored.byteSize,\n      sha256: stored.sha256,\n      originalFilename: stored.originalFilename,\n      createdByAgentId: input.binding.agentId,\n      createdByRunId: input.binding.runId,\n    });\n    if (\n      !attachment.artifactWorkProductId ||\n      attachment.originatingRunId !== input.binding.runId\n    ) {\n      throw new Error(\"paperclip_runner_chat_attachment_origin_not_persisted\");\n    }","sourceCodeStart":1432,"sourceCodeEnd":1468,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L1432-L1468","documentation":"After re-uploading the source bytes to the issue namespace via putStorageObjectWithin, the code verifies the storage write result (stored.byteSize, stored.sha256, stored.contentType) against the verified body and the source metadata. If any field disagrees, this error is thrown and the new storage object is not linked as an attachment. It ensures the storage layer actually persisted exactly the bytes it was given with the intended content type.","triggerScenarios":"storage.putFile returns metadata that disagrees with the write: byteSize != body.length, sha256 differs from input.source.sha256, or contentType differs from input.source.contentType — e.g. the storage service normalized/overrode the content type, a custom StorageService in tests returns canned metadata, or the putFile implementation reports pre-compression or multipart metrics.","commonSituations":"A custom or mock StorageService returning incorrect sha256/byteSize in its putFile result; a storage backend that rewrites content-type (e.g. defaults to application/octet-stream or appends charset); an adapter computing sha256 before transforming the body (compression/encryption) while metadata was compared against the raw source; case differences in contentType strings.","solutions":["Fix the StorageService.putFile implementation to report byteSize/sha256/contentType computed from the exact bytes it persisted","Pass contentType exactly as stored (avoid backends silently rewriting it); if the backend normalizes, propagate its returned value consistently","In tests, make mock putFile echo the real input body's length/hash/contentType instead of hardcoded values","Log the stored vs expected values on mismatch to identify which field diverges before retrying"],"exampleFix":"// before (mock storage returning canned metadata)\nasync putFile(input) { return { provider: \"local\", objectKey: key, byteSize: 0, sha256: \"\", contentType: \"application/octet-stream\", ... }; }\n// after\nasync putFile(input) {\n  await write(key, input.body);\n  return { provider: \"local\", objectKey: key, byteSize: input.body.length, sha256: createHash(\"sha256\").update(input.body).digest(\"hex\").toLowerCase(), contentType: input.contentType, originalFilename: input.originalFilename };\n}","handlingStrategy":"validation","validationCode":"const expectedSha = createHash(\"sha256\").update(body).digest(\"hex\").toLowerCase();\n// after putFile returns `stored`:\nif (stored.byteSize !== body.length || stored.sha256.toLowerCase() !== expectedSha || stored.contentType !== source.contentType)\n  throw new Error(\"storage write metadata diverges from written bytes\");","typeGuard":null,"tryCatchPattern":"try {\n  await prepareReusedChatAttachment({ db, binding, source, title });\n} catch (err) {\n  if (err.message === \"paperclip_runner_chat_attachment_storage_mismatch\") {\n    // delete the orphaned stored object and retry with a corrected StorageService\n    await storage.deleteObject(binding.companyId, candidateObjectKey).catch(() => undefined);\n  } else throw err;\n}","preventionTips":["Make StorageService.putFile return metadata computed from the bytes it actually persisted","Avoid storage backends that rewrite or default content types; propagate the returned contentType","Keep test/mocks faithful: echo real length/hash/contentType from the input body","On mismatch, clean up the orphaned object key before retrying"],"tags":["storage","integrity","verification","attachments"],"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"}