{"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":1088,"sourceCode":"\n    anyhow::ensure!(\n        response.status().is_success(),\n        \"Failed to request models: {}\",\n        response.status()\n    );\n    let mut body = Vec::new();\n    response.body_mut().read_to_end(&mut body).await?;\n\n    let body_str = std::str::from_utf8(&body)?;\n\n    let models = serde_json::from_str::<ModelSchema>(body_str)?.data;\n\n    Ok(models)\n}\n\nasync fn stream_completion(\n    client: Arc<dyn HttpClient>,\n    oauth_token: String,\n    completion_url: Arc<str>,\n    request: Request,\n    is_user_initiated: bool,\n    location: ChatLocation,\n) -> Result<BoxStream<'static, Result<ResponseEvent>>> {\n    let is_vision_request = request.messages.iter().any(|message| match message {\n        ChatMessage::User { content }\n        | ChatMessage::Assistant { content, .. }\n        | ChatMessage::Tool { content, .. } => {\n            matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))\n        }\n        _ => false,\n    });\n\n    let request_builder = copilot_request_headers(\n        HttpRequest::builder()\n            .method(Method::POST)\n            .uri(completion_url.as_ref()),","sourceCodeStart":1070,"sourceCodeEnd":1106,"githubUrl":"https://github.com/zed-industries/zed/blob/916fc2b8cb3a815cbef4a3b40e13081be72036b6/crates/copilot_chat/src/copilot_chat.rs#L1070-L1106","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":"916fc2b8cb3a815cbef4a3b40e13081be72036b6","analyzedAt":"2026-08-20T19:29:52.058Z","contentChangedAt":"2026-08-20T19:29:52.058Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}