paperclipai/paperclip · error · Error

paperclip_runner_file_handoff_storage_mismatch

paperclip_runner_file_handoff_storage_mismatch

Error message

paperclip_runner_file_handoff_storage_mismatch

What it means

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.

Solutions

  1. 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.
  2. Verify the storage backend is not transcoding/compressing content; disable content-type normalization or set it explicitly on upload.
  3. Check stored object integrity directly (download and hash it) to distinguish a metadata bug from real corruption.
  4. If a custom storage wrapper lowercases/uppercases hashes inconsistently, normalize both sides with .toLowerCase() before comparison.

Example fix

// before (custom storage service)
return { byteSize: Buffer.byteLength(body).valueOf(), sha256: hash.toUpperCase(), contentType: 'application/octet-stream' };
// after
return { byteSize: body.length, sha256: createHash('sha256').update(body).digest('hex'), contentType: requestedContentType };
Defensive patterns

Strategy: validation

Validate before calling

// verify storage round-trip before handing the result to the handoff flow
const stored = await storage.putFile(req);
const ok = stored.byteSize === req.body.length
  && stored.sha256.toLowerCase() === expectedSha256
  && stored.contentType === req.contentType;
if (!ok) {
  await storage.deleteObject(req.companyId, stored.objectKey).catch(() => undefined);
  throw new Error('storage returned metadata that mismatches the uploaded body');
}

Try / catch

try {
  await prepareNativeRunnerFileHandoff(input);
} catch (e) {
  if (e.message === 'paperclip_runner_file_handoff_storage_mismatch') {
    // the object is auto-deleted by the catch path; inspect/fix StorageService then retry
    await auditStorageService();
    return retryHandoffWithDirectVerification(input);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/5119009a3a2cace9. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/native-runner-file-handoff.ts:1243

      rollbackDefinitePreCommitFailure: null,
    };
  }

  const storage = input.storage ?? getStorageService();
  const stored = await storage.putFile({
    companyId: input.binding.companyId,
    namespace: `issues/${input.binding.issueId}`,
    originalFilename: verified.filename,
    contentType: verified.contentType,
    body: verified.body,
  });
  try {
    if (
      stored.byteSize !== verified.body.length ||
      stored.sha256.toLowerCase() !== verified.sha256 ||
      stored.contentType !== verified.contentType
    ) {
      throw new Error("paperclip_runner_file_handoff_storage_mismatch");
    }
    const attachment = await issueService(input.db).createAttachment({
      issueId: input.binding.issueId,
      provider: stored.provider,
      objectKey: stored.objectKey,
      contentType: stored.contentType,
      byteSize: stored.byteSize,
      sha256: stored.sha256,
      originalFilename: stored.originalFilename,
      createdByAgentId: input.binding.agentId,
      createdByRunId: input.binding.runId,
    });
    if (
      attachment.originatingRunId !== input.binding.runId ||
      !attachment.artifactWorkProductId
    ) {
      throw new Error("paperclip_runner_file_handoff_origin_not_persisted");
    }

View on GitHub (pinned to 3f1d897a7c)