zed-industries/zed · error
Failed to connect to API
Error message
Failed to connect to API: {} {} What it means
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.
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
Example fix
// before: fire-and-forget call surfaces the raw bail
let stream = stream_completion(client, token, url, request, true, location).await?;
// after: retry transient statuses, fail fast on auth/entitlement
let mut attempt = 0;
loop {
attempt += 1;
match stream_completion(client.clone(), token.clone(), url.clone(), request.clone(), true, location).await {
Ok(stream) => break stream,
Err(err) if attempt < 3 && err.to_string().contains("429") => {
smol::Timer::after(Duration::from_secs(2u64.pow(attempt))).await;
}
Err(err) if err.to_string().contains("401") => return Err(err.context("re-authenticate Copilot")),
Err(err) => return Err(err),
}
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight: refresh the token before sending if it is near expiry
if oauth_token.expires_in_remaining() < Duration::from_secs(30) {
oauth_token = refresh_copilot_token(client).await?;
} Try / catch
match stream_completion(/* .. */).await {
Ok(stream) => { /* handle stream */ }
Err(err) => {
let msg = err.to_string();
if msg.contains("401") {
// trigger re-auth flow
} else if msg.contains("429") || msg.contains("50") {
// schedule backoff retry
} else {
return Err(err);
}
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Failed to connect to API
- The Copilot sign-in code expired. Please try again.
- Baseten API returned
- Copilot sign-in failed
- Copilot sign-in failed: unexpected response from GitHub
AI-assisted analysis of zed-industries/zed@916fc2b8cb (2026-08-20).
Data as JSON: /api/errors/3a07d51521f3012a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/copilot_chat/src/copilot_chat.rs:1088
anyhow::ensure!(
response.status().is_success(),
"Failed to request models: {}",
response.status()
);
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let body_str = std::str::from_utf8(&body)?;
let models = serde_json::from_str::<ModelSchema>(body_str)?.data;
Ok(models)
}
async fn stream_completion(
client: Arc<dyn HttpClient>,
oauth_token: String,
completion_url: Arc<str>,
request: Request,
is_user_initiated: bool,
location: ChatLocation,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.iter().any(|message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_builder = copilot_request_headers(
HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref()),View on GitHub (pinned to 916fc2b8cb)