{"record":{"id":"1fc8a7cd6a052837","repo":"paperclipai/paperclip","slug":"createos-file-download-has-no-body","errorCode":null,"errorMessage":"CreateOS file download has no body.","messagePattern":"CreateOS file download has no body\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/file-sync.ts","lineNumber":86,"sourceCode":"    assertRemotePath(cwd);\n    const result = await execute(client, { ...params, command: \"/bin/bash\", args: [\"-c\", command], cwd },\n      timeoutMs == null ? signal : AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]));\n    if (result.timedOut || result.exitCode !== 0) throw new Error(\"CreateOS transfer command failed.\");\n  };\n  const upload = async (local: string, remote: string) => {\n    const source = createReadStream(local);\n    try {\n      const init: RequestInit & { duplex: \"half\" } = {\n        method: \"PUT\", body: Readable.toWeb(source) as ReadableStream<Uint8Array>,\n        duplex: \"half\", headers: { \"Content-Type\": \"application/octet-stream\" }, signal,\n      };\n      const response = await client.request(`/sandboxes/${id}/files?path=${encodeURIComponent(remote)}`, init);\n      await response.body?.cancel();\n    } finally { source.destroy(); }\n  };\n  const download = async (remote: string, local: string, mode = 0o600) => {\n    const response = await client.request(`/sandboxes/${id}/files?path=${encodeURIComponent(remote)}`, { signal });\n    if (!response.body) throw new Error(\"CreateOS file download has no body.\");\n    await pipeline(response.body, createWriteStream(local, { flags: \"wx\", mode }), { signal });\n  };\n\n  // Validate every mapping before beginning side effects. Host paths are\n  // orchestrator-authored and checked by its source/target-root guard.\n  for (const operation of params.operations) {\n    for (const mapping of operation.files) {\n      if (![\"file\", \"directory\"].includes(mapping.kind)) throw new Error(\"Unsupported CreateOS transfer kind.\");\n      if (!path.isAbsolute(direction === \"in\" ? mapping.sourcePath : mapping.targetPath)) throw new Error(\"CreateOS transfer requires an absolute host path.\");\n      if (mapping.mode != null && (!Number.isInteger(mapping.mode) || mapping.mode < 0 || mapping.mode > 0o777)) throw new Error(\"Invalid CreateOS file mode.\");\n      assertRemotePath(direction === \"in\" ? mapping.targetPath : mapping.sourcePath);\n    }\n    for (const command of operation.postUploadCommands ?? []) {\n      assertRemotePath(command.cwd ?? ROOT);\n      if (command.timeoutMs != null && (!Number.isInteger(command.timeoutMs) || command.timeoutMs < 1 || command.timeoutMs > 86_400_000)) throw new Error(\"Invalid CreateOS transfer timeout.\");\n    }\n    if (direction === \"out\" && operation.postUploadCommands?.length) throw new Error(\"Outbound CreateOS transfers cannot run post-upload commands.\");\n  }","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/file-sync.ts#L68-L104","documentation":"When downloading a file from the CreateOS sandbox via `GET /sandboxes/{id}/files`, the plugin requires the HTTP response to carry a streaming body. If `response.body` is null/undefined, there is nothing to pipe into the local file, so this error is thrown. It typically indicates the provider returned an unexpected response (e.g. an error response without content) or the client implementation stripped the body.","triggerScenarios":"download() is called for a remote path and client.request() resolves with a response whose `body` is null — e.g. the provider returned a 204/error-shaped response, the path is a directory or missing but the API did not surface an error status, or a custom/mock client does not populate body.","commonSituations":"Requesting a remote path that does not exist or is a directory; provider API version change altering the files endpoint response; misconfigured apiUrl pointing at a proxy that drops bodies; testing with a stubbed CreateosClient.","solutions":["Verify the remote path exists and is a regular file inside the sandbox before syncing (run `test -f <path>` via the exec endpoint).","Check that apiUrl points at the correct CreateOS API and that no proxy strips response bodies.","Confirm the CreateosClient/API surface version matches the provider API; update the plugin or client if the endpoint contract changed."],"exampleFix":"// before: blind download\nawait download(\"/work/out.json\", \"/local/out.json\");\n// after: verify it is a file first\nawait run(`test -f /work/out.json`);\nawait download(\"/work/out.json\", \"/local/out.json\");","handlingStrategy":"try-catch","validationCode":"// verify the remote path is a regular file before downloading\nawait run(`test -f ${shellQuote(remote)}`);","typeGuard":null,"tryCatchPattern":"try {\n  await download(remote, local);\n} catch (err) {\n  if (err.message.includes(\"no body\")) {\n    // treat as missing/invalid remote path; check via exec before retrying\n  } else throw err;\n}","preventionTips":["Always verify remote file existence before download","Keep CreateosClient and provider API versions in sync","Avoid proxies that can strip response bodies"],"tags":["sandbox","file-download","http","unexpected-response"],"backgroundTag":"empty-response-body","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}