{"record":{"id":"98d5e3ece5319c16","repo":"Hmbown/CodeWhale","slug":"cloud-agent-sandbox-teardown-failed-http-status","errorCode":null,"errorMessage":"Cloud agent sandbox teardown failed (HTTP {status}).","messagePattern":"Cloud agent sandbox teardown failed \\(HTTP (.+?)\\)\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/cloud_dispatch.rs","lineNumber":1780,"sourceCode":"    fn teardown(&self, receipt: &SandboxReceipt) -> Result<()> {\n        let api_key = Self::api_key()?;\n        if !valid_sandbox_id(&receipt.sandbox_id) {\n            bail!(\"the sandbox id is not a usable path token\");\n        }\n        let url = Self::control_plane_url(&format!(\"sandbox/{}\", receipt.sandbox_id))?;\n        let response = Self::send_json(\n            reqwest::Method::DELETE,\n            &url,\n            &api_key,\n            serde_json::Value::Null,\n        )?;\n        // Daytona returns 204 on delete; treat 404 as already-gone success so\n        // cancel/complete teardown is idempotent.\n        let status = response.status();\n        if status.is_success() || status.as_u16() == 404 {\n            Ok(())\n        } else {\n            bail!(\"Cloud agent sandbox teardown failed (HTTP {status}).\");\n        }\n    }\n\n    fn list_job_sandboxes(&self) -> Result<Vec<LabeledSandbox>> {\n        let api_key = Self::api_key()?;\n        // The provider's list call takes a JSON-encoded exact-match labels\n        // filter (same OpenAPI family as create/get/delete above); filtering\n        // on the product tag keeps the response to Codewhale dispatch\n        // sandboxes only — never the user's own sandboxes on a shared key.\n        let mut url = Self::control_plane_url(\"sandbox\")?;\n        url.query_pairs_mut().append_pair(\n            \"labels\",\n            &format!(\"{{\\\"{SANDBOX_PRODUCT_LABEL}\\\":\\\"{SANDBOX_PRODUCT_VALUE}\\\"}}\"),\n        );\n        let response = Self::send_json(\n            reqwest::Method::GET,\n            &url,\n            &api_key,","sourceCodeStart":1762,"sourceCodeEnd":1798,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/cloud_dispatch.rs#L1762-L1798","documentation":"Thrown by `teardown` when the control-plane `DELETE sandbox/{id}` returns neither a success status nor 404. 2xx and 404 are treated as idempotent success (the comment notes Daytona returns 204 on delete and 404 means already gone); any other status — typically 401/403/429/5xx — means the sandbox was not confirmed deleted. The sandbox may therefore still be running and incurring provider cost.","triggerScenarios":"Canceling or completing a cloud job when the DELETE call fails: expired/invalid API key (401), rate limiting (429), control-plane outage (5xx), or a conflict status if the provider disallows deleting a sandbox in its current state.","commonSituations":"API key rotated or revoked between job start and teardown; provider rate limits hit during mass job cancellation; transient Daytona outage leaving many orphaned sandboxes.","solutions":["Retry the teardown — DELETE is idempotent here, so re-invoking later is safe and will 404 once gone.","Check the HTTP status: 401/403 means fix CODEWHALE cloud credentials (`Self::api_key`) before retrying; 429 means back off and retry.","If it keeps failing, delete the sandbox by id in the provider dashboard to stop billing, then retry teardown to reconcile local state.","Audit for orphaned sandboxes from prior failed teardowns via `list_job_sandboxes` and clean them up."],"exampleFix":"// caller: teardown is idempotent, so retry with backoff\nfor attempt in 0..3 {\n    match provider.teardown(&receipt) {\n        Ok(()) => break,\n        Err(e) if attempt < 2 => std::thread::sleep(Duration::from_secs(2 * (attempt + 1))),\n        Err(e) => return Err(e),\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// DELETE is idempotent (2xx or 404 both succeed), so retry with backoff\nfor attempt in 0..3 {\n    match provider.teardown(&receipt) {\n        Ok(()) => break,\n        Err(e) if attempt == 2 => {\n            log_orphan_sandbox(&receipt.sandbox_id); // clean up manually\n        }\n        Err(_) => std::thread::sleep(Duration::from_secs(2u64.pow(attempt))),\n    }\n}","preventionTips":["Always retry failed teardowns — 2xx and 404 are both success, so retries are safe.","Run a periodic reconciliation pass via list_job_sandboxes to reap orphans.","Rotate API keys on a schedule so teardown-time 401s are rare.","Back off on 429 instead of hammering DELETE during mass cancellations."],"tags":["http","cloud","teardown","resource-cleanup"],"backgroundTag":"http-error-response","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}