{"record":{"id":"65b3fdb5ced7c725","repo":"Hmbown/CodeWhale","slug":"cnb-did-not-report-a-pull-request-number-refusing-to-invent","errorCode":null,"errorMessage":"CNB did not report a pull request number; refusing to invent a URL.","messagePattern":"CNB did not report a pull request number; refusing to invent a URL\\.","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/dispatch_runner.rs","lineNumber":700,"sourceCode":"            \"head\": job.branch,\n            \"base\": patch.base_branch,\n            \"body\": body,\n        }))\n        .send()\n        .context(\"could not reach CNB\")?;\n    let status = response.status();\n    let text = response.text().unwrap_or_default();\n    if !status.is_success() {\n        bail!(\"CNB pull request create failed (HTTP {status}).\");\n    }\n    let parsed: serde_json::Value =\n        serde_json::from_str(&text).context(\"CNB returned invalid JSON\")?;\n    let number = parsed\n        .get(\"number\")\n        .and_then(serde_json::Value::as_i64)\n        .filter(|number| *number > 0)\n        .ok_or_else(|| {\n            anyhow!(\"CNB did not report a pull request number; refusing to invent a URL.\")\n        })?;\n    Ok(format!(\"https://cnb.cool/{slug}/-/pulls/{number}\"))\n}\n\n/// Read a forge token from the Codewhale service slot. Never logged.\nfn read_service_token(slot: &str) -> Option<String> {\n    codewhale_secrets::Secrets::auto_detect()\n        .get(slot)\n        .ok()\n        .flatten()\n        .map(|value| value.trim().to_string())\n        .filter(|value| !value.is_empty())\n}\n\nfn one_line(value: &str, max: usize) -> String {\n    let flat: String = value\n        .chars()\n        .map(|ch| {","sourceCodeStart":682,"sourceCodeEnd":718,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/dispatch_runner.rs#L682-L718","documentation":"After POSTing to the CNB API, open_pr_cnb parses the JSON response and extracts the `number` field to build the PR URL. If the field is absent, not an integer, or <= 0, it refuses to fabricate a URL and fails. This is an internal-invariant check against silently producing a broken pull-request link.","triggerScenarios":"CNB API returned 2xx JSON without a positive integer `number` field — e.g. an unexpected response shape after an API change, or a proxy returning a non-PR JSON body.","commonSituations":"CNB server version drift changing the response schema; an auth middleware returning a JSON error body with HTTP 200; hitting the wrong endpoint via a misconfigured cnb_pr_url.","solutions":["Inspect the raw CNB response text (it is captured in the context chain) to see what was actually returned.","Confirm the CNB endpoint URL and API version; update Codewhale if CNB changed its response schema.","Retry the request; if the server intermittently returns malformed bodies, check for proxies/MITM in front of cnb.cool."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"let parsed: serde_json::Value = serde_json::from_str(&text)?;\nif parsed.get(\"number\").and_then(|n| n.as_i64()).filter(|n| *n > 0).is_none() {\n    eprintln!(\"CNB response lacks a valid PR number: {text}\");\n}","typeGuard":"fn pr_number(v: &serde_json::Value) -> Option<i64> {\n    v.get(\"number\").and_then(serde_json::Value::as_i64).filter(|n| *n > 0)\n}","tryCatchPattern":"match open_pr_cnb(...) {\n    Err(e) if e.to_string().contains(\"did not report a pull request number\") => {\n        log::warn!(\"CNB returned an unexpected body; inspect raw response before retrying\");\n    }\n    other => other?,\n}","preventionTips":["Log (redacted) raw CNB responses during integration to detect schema drift early.","Pin/test against the CNB API version you target.","Check for proxies returning 200 with error bodies."],"tags":["network","api","pull-request","cnb"],"backgroundTag":"unexpected-response-shape","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}