{"record":{"id":"f432e6665b660e7f","repo":"paperclipai/paperclip","slug":"exe-dev-api-command-failed-response-status-fo","errorCode":null,"errorMessage":"exe.dev API command failed (${response.status}) for: ${logCommand}","messagePattern":"exe\\.dev API command failed \\((.+?)\\) for: (.+?)","errorType":"exception","errorClass":"ExeDevApiError","httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/exe-dev/src/plugin.ts","lineNumber":388,"sourceCode":"}\n\nasync function runLifecycleCommand(\n  config: ExeDevDriverConfig,\n  command: string,\n  logCommand = command,\n): Promise<unknown> {\n  const response = await fetch(config.apiUrl, {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${resolveApiKey(config)}`,\n      \"Content-Type\": \"text/plain; charset=utf-8\",\n    },\n    body: command,\n    signal: AbortSignal.timeout(Math.min(config.timeoutMs, EXE_DEV_API_MAX_TIMEOUT_MS)),\n  });\n  const body = await response.text();\n  if (!response.ok) {\n    throw new ExeDevApiError(\n      `exe.dev API command failed (${response.status}) for: ${logCommand}`,\n      response.status,\n      body,\n    );\n  }\n\n  const trimmed = body.trim();\n  if (!trimmed) return null;\n  try {\n    return JSON.parse(trimmed);\n  } catch {\n    return body;\n  }\n}\n\nfunction parseVmRecord(value: unknown, depth = 0): ExeDevVmRecord | null {\n  if (depth > MAX_VM_RECORD_DEPTH) return null;\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;","sourceCodeStart":370,"sourceCodeEnd":406,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/plugins/sandbox-providers/exe-dev/src/plugin.ts#L370-L406","documentation":"Thrown as ExeDevApiError by runLifecycleCommand when the POST to config.apiUrl returns a non-2xx response. The error carries .status (the HTTP code) and .body (the raw response text) so callers can branch on the cause. The logCommand in the message is the redacted command string (redactCreateCommand strips --env, --prompt, --setup-script values) to keep secrets out of logs.","triggerScenarios":"exe.dev API rejecting the lifecycle command: 401/403 from a bad or expired API key, 404 from a wrong apiUrl, 400 from a malformed command (bad image/name/flags), 5xx from an exe.dev outage, or a timeout surfaced as a non-OK response.","commonSituations":"API key revoked or rotated but config not updated; apiUrl pointing at the wrong endpoint; vm name over 63 chars from buildVmName colliding with an existing name; image/command flags not supported by the exe.dev CLI version; transient exe.dev 5xx during a deploy.","solutions":["Inspect error.status and error.body: 401/403 -> fix the API key (EXE_API_KEY or config.apiKey); 404 -> correct apiUrl; 400 -> correct command flags/image.","For 5xx or transient network errors, retry with backoff (the provider already enforces AbortSignal.timeout capped by EXE_DEV_API_MAX_TIMEOUT_MS).","Print the redacted logCommand from the error message to see which lifecycle op and flags failed without leaking secrets."],"exampleFix":"// before\nawait runLifecycleCommand(config, cmd);\n\n// after\ntry {\n  await runLifecycleCommand(config, cmd);\n} catch (err) {\n  if (err instanceof ExeDevApiError) {\n    log.error({ status: err.status, body: err.body }, 'exe.dev lifecycle failed');\n    if (err.status === 401 || err.status === 403) throw new Error('exe.dev auth failed');\n    if (err.status >= 500) throw new RetryableError(err.message);\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"function isExeDevConfigValid(config: ExeDevDriverConfig): string | null {\n  if (!config.apiUrl || !/^https?:\\/\\//.test(config.apiUrl)) return 'apiUrl missing/invalid';\n  if (!(config.apiKey || process.env.EXE_API_KEY)) return 'apiKey missing';\n  return null;\n}","typeGuard":"function isExeDevApiError(x: unknown): x is { status: number; body: string; message: string } {\n  return x instanceof Error && (x as { name?: string }).name === 'ExeDevApiError';\n}","tryCatchPattern":"try {\n  await runLifecycleCommand(config, cmd);\n} catch (err) {\n  if (err instanceof Error && (err as { name?: string }).name === 'ExeDevApiError') {\n    const e = err as { status: number; body: string };\n    if (e.status === 401 || e.status === 403) throw new Error('exe.dev auth failed');\n    if (e.status >= 500) throw new RetryableError(e.body || 'exe.dev 5xx');\n    throw new Error(`exe.dev ${e.status}: ${e.body}`);\n  }\n  throw err;\n}","preventionTips":["Always branch on err.status; do not string-match the message.","Log err.body for diagnosis but redact any echoed secrets.","Cap retries on 5xx; treat 4xx as non-retryable except 429."],"tags":["exe-dev","http","api-error","auth","retry","sandbox"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}