zed-industries/zed · error · anyhow::Error

failed to fetch '{url}': status code {}

Error message

failed to fetch '{url}': status code {}

What it means

Same guard as in the v0.1.0 bindings, present in the v0.8.0 host: http_client::fetch bails whenever the response status is a client or server error, embedding the requested URL and the status. The v0.8.0 fetch, like v0.1.0, returns Ok(HttpResponse) only for 2xx/3xx; the error string is surfaced to the extension as the Err side of its Result. fetch_stream remains the escape hatch for reading non-2xx bodies.

Source

Thrown at crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs:639

        let delegate = self.table.get(&delegate)?;
        Ok(delegate.which(binary_name).await)
    }

    async fn drop(&mut self, _worktree: Resource<Worktree>) -> wasmtime::Result<()> {
        // We only ever hand out borrows of worktrees.
        Ok(())
    }
}

impl common::Host for WasmState {}

impl http_client::Host for WasmState {
    async fn fetch(
        &mut self,
        request: http_client::HttpRequest,
    ) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
        maybe!(async {
            let url = &request.url;
            let request = convert_request(&request)?;
            let mut response = self.host.http_client.send(request).await?;

            if response.status().is_client_error() || response.status().is_server_error() {
                bail!("failed to fetch '{url}': status code {}", response.status())
            }
            convert_response(&mut response).await
        })
        .await
        .to_wasmtime_result()
    }

    async fn fetch_stream(
        &mut self,
        request: http_client::HttpRequest,
    ) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
        let request = convert_request(&request).into_wasmtime_result()?;
        let response = self.host.http_client.send(request);

View on GitHub (pinned to 5a9b9558db)

Solutions

  1. curl -I the exact URL from the message to see the real status and act on it (fix URL, add auth header, wait out rate limit)
  2. Switch to fetch_stream when the error body or status must be inspected
  3. Add retry with backoff for 5xx/429 responses
  4. Cache downloaded assets so transient failures degrade gracefully

Example fix

// before
let response = http_client::fetch(&request).await?; // Err("failed to fetch '...': status code 403 Forbidden")

// after
match http_client::fetch(&request).await {
    Ok(response) => Ok(response),
    Err(e) if e.contains("status code 4") || e.contains("status code 5") => {
        Err(format!("download failed ({e}); check URL/auth"))
    }
    Err(e) => Err(e),
}
Defensive patterns

Strategy: try-catch

Try / catch

match http_client::fetch(&request).await {
    Ok(response) => Ok(response),
    Err(e) if e.contains("status code 4") || e.contains("status code 5") => {
        Err(format!("non-2xx response: {e}"))
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: An extension on the v0.8.0 API calling http_client::fetch against any URL returning 4xx/5xx (404 release asset, 403 rate-limited API, 500 server error).

Common situations: Download URLs that 404 after an upstream release rename, GitHub API secondary rate limits returning 403, expired tokens, or corporate proxies answering 502.

Related errors


AI-assisted analysis of zed-industries/zed@5a9b9558db (2026-08-20). Data as JSON: /api/errors/f35032d9e18073f8. Report an issue: GitHub.