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

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

Error message

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

What it means

Thrown by Zed's WebAssembly extension host (v0.1.0 WIT bindings) when an extension calls http_client::fetch and the server responds with a 4xx or 5xx status. The host deliberately converts any non-success HTTP status into an error string instead of returning the response, because the v0.1.0 WIT HttpResponse type has no way to carry an error body. Note that fetch_stream does NOT bail this way, so error responses with bodies must go through that API.

Source

Thrown at crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs:312

    async fn which(
        &mut self,
        delegate: Resource<Arc<dyn WorktreeDelegate>>,
        binary_name: String,
    ) -> wasmtime::Result<Option<String>> {
        latest::HostWorktree::which(self, delegate, 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,

View on GitHub (pinned to 5a9b9558db)

Solutions

  1. Verify the exact URL the extension requests (it is echoed in the message) in a browser or with curl -I
  2. Handle 4xx/5xx bodies by switching to fetch_stream, which returns the response regardless of status
  3. For auth failures, supply the needed token/header in HttpRequest headers or use a public URL
  4. For rate limits, retry with backoff or use a conditional/etag-aware endpoint

Example fix

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

// after — read error bodies via fetch_stream
let stream = http_client::fetch_stream(&request).await?;
let response = stream.next().await; // status + body available even for 4xx/5xx
Defensive patterns

Strategy: try-catch

Try / catch

// extension side: fetch returns Result<HttpResponse, String>
match http_client::fetch(&request).await {
    Ok(response) => { /* 2xx/3xx only */ }
    Err(e) if e.contains("status code") => {
        // non-2xx: surface URL+status, fall back to cached asset or retry for 5xx
    }
    Err(e) => return Err(e), // transport-level failure
}

Prevention

When it happens

Trigger: A v0.1.0 extension calling http_client::fetch against a URL that returns 404, 401, 403, 500, 502, etc. The bail fires inside the maybe! block when response.status().is_client_error() || response.status().is_server_error().

Common situations: Extension downloading a release asset from a moved/renamed GitHub URL, hitting an API rate limit (403/429), an expired pre-signed S3 link, a typo in the download URL, a proxy returning 502, or auth missing for a private repo.

Related errors


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