{"record":{"id":"8a2b795d4ac82e32","repo":"warpdotdev/warp","slug":"artifact-download-failed-err","errorCode":null,"errorMessage":"Artifact download failed: {err}","messagePattern":"Artifact download failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/ai/artifact_download.rs","lineNumber":92,"sourceCode":"        tokio::fs::create_dir_all(parent).await.with_context(|| {\n            format!(\"Failed to create download directory '{}'\", parent.display())\n        })?;\n    }\n\n    let response = http_client\n        .get(artifact.download_url())\n        .timeout(Duration::from_secs(300))\n        .send()\n        .await\n        .with_context(|| {\n            format!(\n                \"Failed to download artifact '{}' from signed URL\",\n                artifact.artifact_uid()\n            )\n        })?;\n    let response = response\n        .error_for_status()\n        .map_err(|err| anyhow!(\"Artifact download failed: {err}\"))?;\n\n    let mut file = tokio::fs::File::create(path)\n        .await\n        .with_context(|| format!(\"Failed to create download file '{}'\", path.display()))?;\n    let mut response_stream =\n        StreamReader::new(response.bytes_stream().map_err(std::io::Error::other));\n    tokio::io::copy(&mut response_stream, &mut file)\n        .await\n        .with_context(|| format!(\"Failed to write download file '{}'\", path.display()))?;\n    file.sync_data()\n        .await\n        .with_context(|| format!(\"Failed to sync download file '{}'\", path.display()))?;\n\n    Ok(())\n}\n\n#[cfg(test)]\n#[path = \"artifact_download_tests.rs\"]","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/warpdotdev/warp/blob/e72fd7aacbbb2236d9b3be2aad7e7178fe94b4bc/app/src/ai/artifact_download.rs#L74-L110","documentation":"The artifact download GET to artifact.download_url() succeeded at the transport layer but error_for_status() saw a non-success HTTP status; the wrapped status error is surfaced with this message. Signed artifact URLs are short-lived, so a 403 from an expired signature is the canonical cause.","triggerScenarios":"reqwest GET on the signed URL returns 4xx/5xx: 403 expired/mismatched signature, 404 deleted or nonexistent artifact_uid, 5xx from the object store / CDN (artifact_download.rs:87-92).","commonSituations":"Delay between obtaining the signed URL and starting the download exceeds the URL TTL; URL fetched through a rewriting proxy that strips query params; artifact deleted before download; transient CDN errors.","solutions":["Re-fetch the artifact metadata to mint a fresh signed URL and retry the download","Verify the artifact_uid still exists server-side before retrying","Start the download immediately after obtaining the URL instead of queueing it","Check corporate proxies/VPCs that rewrite or cache signed URLs"],"exampleFix":"// before\nlet response = client.get(url.clone()).send().await?;\nlet response = response.error_for_status().map_err(|err| anyhow!(\"Artifact download failed: {err}\"))?;\n\n// after - refresh the signed URL once on status failure\nlet mut response = client.get(url.clone()).send().await?;\nif !response.status().is_success() {\n    let fresh = artifact_api.refetch_download_url(artifact.artifact_uid()).await?;\n    response = client.get(fresh).send().await?;\n}\nlet response = response.error_for_status().map_err(|err| anyhow!(\"Artifact download failed: {err}\"))?;","handlingStrategy":"retry","validationCode":"if let Some(expiry) = url_expires_at(&url) {\n    if expiry - Instant::now() < Duration::from_secs(30) {\n        let url = artifact_api.refetch_download_url(uid).await?; // refresh before use\n    }\n}","typeGuard":null,"tryCatchPattern":"let resp = match download(artifact).await {\n    Err(e) if e.to_string().starts_with(\"Artifact download failed\") => {\n        let fresh = refetch_url(artifact.artifact_uid()).await?;\n        download_with_url(fresh).await? // one bounded retry with a fresh signed URL\n    }\n    r => r?,\n};","preventionTips":["Download immediately after minting the signed URL; never persist URLs across sessions","Keep the 300s transport timeout but retry once on 4xx/5xx with a newly minted URL","Bypass proxies that rewrite query strings on signed URLs"],"tags":["rust","warp","http","download","signed-url","retry"],"backgroundTag":null,"analyzedSha":"e72fd7aacbbb2236d9b3be2aad7e7178fe94b4bc","analyzedAt":"2026-08-16T08:27:25.381Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}