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
- Open https://github.com/{owner}/{name} in a browser to confirm the repo exists and is public
- On 429, wait for the rate-limit window to reset before retrying discovery
- Clear the custom branch so the main/master fallback can resolve renamed default branches
- 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
- Rate-limit discovery browsing client-side to stay under GitHub's ~60 unauthenticated requests/hour
- Verify repo exists and is public before adding — private repos cannot be fetched by the unauthenticated archive endpoint
- Clear invalid custom branches so the main/master fallback can rescue renamed default branches
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.