{"record":{"id":"3a07d51521f3012a","repo":"zed-industries/zed","slug":"failed-to-connect-to-api","errorCode":null,"errorMessage":"Failed to connect to API: {} {}","messagePattern":"Failed to connect to API: (.+?) (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/copilot_chat/src/copilot_chat.rs","lineNumber":1080,"sourceCode":"        &oauth_token,\n        Some(is_user_initiated),\n        Some(location),\n    )\n    .when(is_vision_request, |builder| {\n        builder.header(\"Copilot-Vision-Request\", is_vision_request.to_string())\n    });\n\n    let is_streaming = request.stream;\n\n    let json = serde_json::to_string(&request)?;\n    let request = request_builder.body(AsyncBody::from(json))?;\n    let mut response = client.send(request).await?;\n\n    if !response.status().is_success() {\n        let mut body = Vec::new();\n        response.body_mut().read_to_end(&mut body).await?;\n        let body_str = std::str::from_utf8(&body)?;\n        anyhow::bail!(\n            \"Failed to connect to API: {} {}\",\n            response.status(),\n            body_str\n        );\n    }\n\n    if is_streaming {\n        let reader = BufReader::new(response.into_body());\n        Ok(reader\n            .lines()\n            .filter_map(|line| async move {\n                match line {\n                    Ok(line) => {\n                        let line = line.strip_prefix(\"data: \")?;\n                        if line.starts_with(\"[DONE]\") {\n                            return None;\n                        }\n","sourceCodeStart":1062,"sourceCodeEnd":1098,"githubUrl":"https://github.com/zed-industries/zed/blob/f4178619acd0d47ea1f76a2025c42962c6d6638c/crates/copilot_chat/src/copilot_chat.rs#L1062-L1098","documentation":"Thrown by stream_completion in Zed's Copilot chat client when the POST to the Copilot chat-completions endpoint (completion_url, with Copilot vision/user-initiated headers) returns a non-2xx status. The message embeds the HTTP status code and the raw response body, which carries the real reason: 401 expired OAuth token, 403 missing Copilot entitlement, 429 rate limiting, or a GitHub 5xx. It fires before any SSE line parsing, so no partial stream content is produced.","triggerScenarios":"Any Copilot chat completion request (request.stream either true or false) sent with: a stale/revoked GitHub Copilot OAuth token (401), a user with no active Copilot subscription (403, body often contains 'copilot_not_enabled'), tripping secondary rate limits (429), oversized prompts rejected (413), or GitHub API incidents (5xx). Vision requests additionally send 'Copilot-Vision-Request: true' and can be rejected when vision is not entitled.","commonSituations":"Token revoked by changing GitHub password or re-authorizing elsewhere; free-tier or lapsed Copilot subscriptions; aggressive retry loops hitting rate limits; corporate proxies returning HTML error pages as the body; requests to a stale cached completion_url.","solutions":["If status is 401: sign out and back into Copilot so a fresh OAuth token is issued","If 403 with entitlement text: verify an active Copilot subscription at github.com/settings/copilot","If 429: back off and retry after the indicated interval; reduce request frequency","Read the embedded body text - it names the exact upstream reason; act on that specific status","If 5xx or proxy HTML: check githubstatus.com and bypass/inspect the proxy"],"exampleFix":"// before: fire-and-forget call surfaces the raw bail\nlet stream = stream_completion(client, token, url, request, true, location).await?;\n\n// after: retry transient statuses, fail fast on auth/entitlement\nlet mut attempt = 0;\nloop {\n    attempt += 1;\n    match stream_completion(client.clone(), token.clone(), url.clone(), request.clone(), true, location).await {\n        Ok(stream) => break stream,\n        Err(err) if attempt < 3 && err.to_string().contains(\"429\") => {\n            smol::Timer::after(Duration::from_secs(2u64.pow(attempt))).await;\n        }\n        Err(err) if err.to_string().contains(\"401\") => return Err(err.context(\"re-authenticate Copilot\")),\n        Err(err) => return Err(err),\n    }\n}","handlingStrategy":"retry","validationCode":"// Pre-flight: refresh the token before sending if it is near expiry\nif oauth_token.expires_in_remaining() < Duration::from_secs(30) {\n    oauth_token = refresh_copilot_token(client).await?;\n}","typeGuard":null,"tryCatchPattern":"match stream_completion(/* .. */).await {\n    Ok(stream) => { /* handle stream */ }\n    Err(err) => {\n        let msg = err.to_string();\n        if msg.contains(\"401\") {\n            // trigger re-auth flow\n        } else if msg.contains(\"429\") || msg.contains(\"50\") {\n            // schedule backoff retry\n        } else {\n            return Err(err);\n        }\n    }\n}","preventionTips":["Refresh the Copilot OAuth token proactively before it expires rather than after a 401","Wrap completions in bounded exponential-backoff retries for 429/5xx only","Log and persist the status/body pair - it distinguishes auth vs entitlement vs capacity instantly"],"tags":["copilot","chat","http","authentication","rate-limit","rust"],"backgroundTag":"http-error-response","analyzedSha":"f4178619acd0d47ea1f76a2025c42962c6d6638c","analyzedAt":"2026-08-20T19:29:52.058Z","schemaVersion":2},"datasetVersion":"2026-08-24T22:17:12.610Z"}