farion1231/cc-switch · error · anyhow::Error

DOWNLOAD_FAILED

DOWNLOAD_FAILED

Error message

{"code":"DOWNLOAD_FAILED","context":{"status":"{status}"},"suggestion":"http403|http404|http429|checkNetwork"}

What it means

Structured error from download_and_extract: the GET on the GitHub archive URL returned a non-2xx status. The suggestion field maps the status — 403→http403, 404→http404, 429→http429, anything else→checkNetwork. Because download_repo tries the custom branch then main then master, the error you finally see is the last candidate's; a wrong custom branch alone usually recovers silently via fallback.

Source

Thrown at src-tauri/src/services/skill.rs:3128

                    // 否则 N 个候选分支等于 N 倍的落盘量堆在同一个目录里。
                    let _ = fs::remove_dir_all(&temp_path);
                    let _ = fs::create_dir_all(&temp_path);
                    last_error = Some(e);
                    continue;
                }
            }
        }

        Err(last_error.unwrap_or_else(|| anyhow::anyhow!("所有分支下载失败")))
    }

    /// 下载并解压 ZIP
    async fn download_and_extract(&self, url: &str, dest: &Path) -> Result<()> {
        let client = crate::proxy::http_client::get();
        let response = client.get(url).send().await?;
        if !response.status().is_success() {
            let status = response.status().as_u16().to_string();
            return Err(anyhow::anyhow!(format_skill_error(
                "DOWNLOAD_FAILED",
                &[("status", &status)],
                match status.as_str() {
                    "403" => Some("http403"),
                    "404" => Some("http404"),
                    "429" => Some("http429"),
                    _ => Some("checkNetwork"),
                },
            )));
        }

        // 逐块读并卡住压缩体大小:`response.bytes()` 会先把攻击者控制的整个归档
        // 收进内存,之后才轮到 ZipArchive 和解压预算——那时候堆已经被吃光了。
        // 不能只信 Content-Length(可以撒谎或缺失),必须按实际收到的字节数算。
        let mut response = response;
        let mut body: Vec<u8> = Vec::new();
        while let Some(chunk) = response.chunk().await? {
            if body.len().saturating_add(chunk.len()) as u64 > MAX_ARCHIVE_DOWNLOAD_BYTES {

View on GitHub (pinned to a2e22f3302)

Solutions

  1. Open https://github.com/{owner}/{name} in a browser to confirm the repo exists and is public
  2. On 429, wait for the rate-limit window to reset before retrying discovery
  3. Clear the custom branch so the main/master fallback can resolve renamed default branches
  4. Check proxy settings if the status is unusual (5xx/407) — the app routes through crate::proxy::http_client

Example fix

// before — treating any download failure the same
match service.fetch_repo_skills(&repo).await {
    Err(e) => log::error!("failed: {e}"),
    ...
}

// after — branch on the structured status
let err = e.to_string();
if err.contains("\"code\":\"DOWNLOAD_FAILED\"") {
    if err.contains("\"status\":\"404\"") { /* tell user repo not found */ }
    else if err.contains("\"status\":\"429\"") { /* back off and retry later */ }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust — pre-flight: confirm the repo is publicly reachable before discovery
async fn repo_exists(owner: &str, name: &str) -> bool {
    crate::proxy::http_client::get()
        .head(format!("https://github.com/{owner}/{name}"))
        .send().await.map(|r| r.status().is_success()).unwrap_or(false)
}

Type guard

export interface SkillError { code: string; context: Record<string, string>; suggestion: string }
export function parseSkillError(e: unknown): SkillError | null {
  if (typeof e !== "string") return null;
  try { const v = JSON.parse(e); return v?.code ? (v as SkillError) : null; } catch { return null; }
}

Try / catch

// TS — branch on the structured status
catch (e) {
  const err = parseSkillError(e);
  if (err?.code === "DOWNLOAD_FAILED") {
    switch (err.context.status) {
      case "404": return showHint("repoNotFound");
      case "429": return scheduleRetry(3600_000); // rate-limit window
      case "403": return showHint("http403");
      default:   return showHint("checkNetwork");
    }
  }
  throw e;
}

Prevention

When it happens

Trigger: 404: repo deleted/renamed, or repo is private (the archive endpoint is unauthenticated) or all three branches missing. 429: GitHub unauthenticated rate limit (~60 req/h per IP). 403: rate limiting or blocked access. Others: proxy/VPN intercepting github.com.

Common situations: Repo renamed by owner; typo in owner/name; heavy discovery browsing exhausting the IP rate limit; corporate proxies returning 407/502 mapped to checkNetwork.

Related errors


AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16). Data as JSON: /api/errors/6cf25fc7d5f5737d. Report an issue: GitHub.