{"record":{"id":"682d1567dd4a7284","repo":"paperclipai/paperclip","slug":"createos-returned-invalid-json","errorCode":null,"errorMessage":"CreateOS returned invalid JSON.","messagePattern":"CreateOS returned invalid JSON\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/client.ts","lineNumber":79,"sourceCode":"        : path.includes(\"/stdin/close\") ? \"stdin close\"\n        : path.includes(\"/connect?\") ? \"output connection\"\n        : path.endsWith(\"/processes\") ? \"process creation\"\n        : path.includes(\"/processes/\") ? \"process cleanup\"\n        : path.endsWith(\"/exec\") ? \"workspace command\"\n        : \"sandbox lifecycle\";\n      throw new CreateosApiError(response.status, operation);\n    }\n    return response;\n  }\n\n  async json(path: string, method = \"GET\", body?: unknown, signal?: AbortSignal): Promise<Record<string, unknown>> {\n    const response = await this.request(path, {\n      method,\n      ...(body !== undefined ? { body: JSON.stringify(body), headers: { \"Content-Type\": \"application/json\" } } : {}),\n      signal,\n    });\n    let envelope: Record<string, unknown>;\n    try { envelope = object(await response.json()); } catch { throw new Error(\"CreateOS returned invalid JSON.\"); }\n    if (envelope.status !== \"success\") throw new Error(\"CreateOS returned an unsuccessful response.\");\n    return object(envelope.data);\n  }\n\n  async getSandbox(id: string, signal?: AbortSignal): Promise<Sandbox> {\n    const data = await this.json(`/sandboxes/${identifier(id)}`, \"GET\", undefined, signal);\n    if (data.id !== id || typeof data.status !== \"string\") throw new Error(\"CreateOS sandbox identity or state is invalid.\");\n    return { id, status: data.status };\n  }\n\n  async createSandbox(signal: AbortSignal): Promise<Sandbox> {\n    const { shape, rootfs, region } = this.config;\n    const data = await this.json(\"/sandboxes\", \"POST\", {\n      shape,\n      ...(rootfs ? { rootfs } : {}),\n      ...(region ? { region } : {}),\n      ingress_enabled: false,\n      // The host owns lease release. Idle pause is not a command timeout or a","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/client.ts#L61-L97","documentation":"Thrown by `json()` when the HTTP 200 response body cannot be parsed as JSON, or parses to a non-object, so the `{status, data}` envelope cannot be built. `response.json()` throwing (malformed/truncated/empty body, HTML error page) and `object()` rejecting a non-object are both collapsed into this single sanitized message — the raw body is never surfaced because it could contain private workspace names or credentials.","triggerScenarios":"An endpoint that returns 200 with an empty body or plain text; an intermediary (auth proxy, HTML login page, CDN error page) replacing the JSON response while keeping status 200; truncated chunked responses; body actually HTML despite a 200; `object()` rejecting a top-level JSON array/null inside the same try block.","commonSituations":"Gateway redirect to an HTML sign-in page with 200; provider returning an empty 200 for accepted-but-no-content operations; misrouted apiUrl hitting a different service; body compression/encoding mismatch introduced by a proxy.","solutions":["Fetch the same endpoint with curl and inspect the raw body and Content-Type to see what is actually returned.","Verify `config.apiUrl` points at the CreateOS API and not a UI/login page or another service.","Check for proxies/gateways that could inject HTML (auth walls, error interstitials) and exclude the API host from them.","Ensure the operation is expected to return a body; if an endpoint legitimately returns 204/empty, call it via `request()` instead of `json()`.","Retry — transient truncation of chunked bodies can produce this; persistent failure indicates a config/routing problem."],"exampleFix":"// before: no way to tell why json() failed\nconst data = await client.json(`/sandboxes/${id}`, \"GET\");\n// after: probe the endpoint's raw response first when debugging\nconst res = await fetch(`${apiUrl}/v1/sandboxes/${id}`, { headers: { \"X-Api-Key\": key } });\nconst text = await res.text();\ntry { JSON.parse(text); } catch {\n  throw new Error(`Non-JSON body from CreateOS (first 200 chars): ${text.slice(0, 200)}`);\n}","handlingStrategy":"try-catch","validationCode":"const res = await fetch(`${config.apiUrl}/v1/sandboxes`, { headers: { \"X-Api-Key\": key } });\nconst ct = res.headers.get(\"content-type\") ?? \"\";\nif (!ct.includes(\"application/json\")) throw new Error(`Unexpected content-type: ${ct}`);","typeGuard":null,"tryCatchPattern":"try {\n  const data = await client.json(`/sandboxes/${id}`, \"GET\");\n} catch (e) {\n  if (e instanceof Error && e.message === \"CreateOS returned invalid JSON.\") {\n    // fetch raw body out-of-band to diagnose (HTML page? empty? truncated?)\n  } else throw e;\n}","preventionTips":["Exclude the API host from auth proxies/captive portals that return HTML with 200.","Assert `content-type: application/json` in integration tests against staging.","Call empty-bodied endpoints via request() directly instead of json().","Monitor for this error appearing after proxy/CDN config changes."],"tags":["json","response-parsing","api","sandbox-provider"],"backgroundTag":"invalid-json-response","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}