{"record":{"id":"5119009a3a2cace9","repo":"paperclipai/paperclip","slug":"paperclip-runner-file-handoff-storage-mismatch","errorCode":"paperclip_runner_file_handoff_storage_mismatch","errorMessage":"paperclip_runner_file_handoff_storage_mismatch","messagePattern":"paperclip_runner_file_handoff_storage_mismatch","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/native-runner-file-handoff.ts","lineNumber":1243,"sourceCode":"      rollbackDefinitePreCommitFailure: null,\n    };\n  }\n\n  const storage = input.storage ?? getStorageService();\n  const stored = await storage.putFile({\n    companyId: input.binding.companyId,\n    namespace: `issues/${input.binding.issueId}`,\n    originalFilename: verified.filename,\n    contentType: verified.contentType,\n    body: verified.body,\n  });\n  try {\n    if (\n      stored.byteSize !== verified.body.length ||\n      stored.sha256.toLowerCase() !== verified.sha256 ||\n      stored.contentType !== verified.contentType\n    ) {\n      throw new Error(\"paperclip_runner_file_handoff_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.originatingRunId !== input.binding.runId ||\n      !attachment.artifactWorkProductId\n    ) {\n      throw new Error(\"paperclip_runner_file_handoff_origin_not_persisted\");\n    }","sourceCodeStart":1225,"sourceCodeEnd":1261,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/native-runner-file-handoff.ts#L1225-L1261","documentation":"After writing the deliverable to storage, prepareNativeRunnerFileHandoff verifies that the StorageService's putFile result actually matches what was requested: byte size, lowercase hex sha256, and content type must equal the verified workspace file. A mismatch means the storage backend corrupted, transformed, or mis-reported the object, so the attachment is not created and the uploaded object is cleaned up.","triggerScenarios":"storage.putFile returns metadata that disagrees with the input body: byteSize !== body.length, sha256 (case-insensitively) differs, or contentType differs from the verified deliverable's content type — e.g. a storage layer that recomputes/mangles content type, applies compression/transcoding, or returns stale metadata.","commonSituations":"Custom StorageService implementation (S3/local/fs) that reports rounded sizes or normalizes content types; a proxy/middleware altering uploads; case-sensitive sha256 comparison hitting a backend returning uppercase hex without toLowerCase in a custom build; storage plugin bug or misconfigured content-type sniffing.","solutions":["Fix the StorageService.putFile implementation to echo back the exact contentType and byteSize of the written body and compute sha256 over the exact bytes written, lowercased.","Verify the storage backend is not transcoding/compressing content; disable content-type normalization or set it explicitly on upload.","Check stored object integrity directly (download and hash it) to distinguish a metadata bug from real corruption.","If a custom storage wrapper lowercases/uppercases hashes inconsistently, normalize both sides with .toLowerCase() before comparison."],"exampleFix":"// before (custom storage service)\nreturn { byteSize: Buffer.byteLength(body).valueOf(), sha256: hash.toUpperCase(), contentType: 'application/octet-stream' };\n// after\nreturn { byteSize: body.length, sha256: createHash('sha256').update(body).digest('hex'), contentType: requestedContentType };","handlingStrategy":"validation","validationCode":"// verify storage round-trip before handing the result to the handoff flow\nconst stored = await storage.putFile(req);\nconst ok = stored.byteSize === req.body.length\n  && stored.sha256.toLowerCase() === expectedSha256\n  && stored.contentType === req.contentType;\nif (!ok) {\n  await storage.deleteObject(req.companyId, stored.objectKey).catch(() => undefined);\n  throw new Error('storage returned metadata that mismatches the uploaded body');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await prepareNativeRunnerFileHandoff(input);\n} catch (e) {\n  if (e.message === 'paperclip_runner_file_handoff_storage_mismatch') {\n    // the object is auto-deleted by the catch path; inspect/fix StorageService then retry\n    await auditStorageService();\n    return retryHandoffWithDirectVerification(input);\n  }\n  throw e;\n}","preventionTips":["Write unit tests for custom StorageService implementations asserting byteSize/sha256/contentType echo the input exactly.","Always lowercase sha256 in storage implementations.","Disable backend content-type normalization/sniffing for issue attachment buckets.","Monitor storage metadata mismatches to catch transcoding proxies early."],"tags":["storage","integrity","checksum","native-runner"],"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"}