{"record":{"id":"149eb45e6fff5664","repo":"neondatabase/neon","slug":"error-downloading-extension","errorCode":null,"errorMessage":"error downloading extension {:?}: {:?}","messagePattern":"error downloading extension (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"compute_tools/src/extension_server.rs","lineNumber":158,"sourceCode":"    panic!(\"Unsuported postgres version {human_version}\");\n}\n\n// download the archive for a given extension,\n// unzip it, and place files in the appropriate locations (share/lib)\npub async fn download_extension(\n    ext_name: &str,\n    ext_path: &RemotePath,\n    remote_ext_base_url: &Url,\n    pgbin: &str,\n) -> Result<u64> {\n    info!(\"Download extension {:?} from {:?}\", ext_name, ext_path);\n\n    // TODO add retry logic\n    let download_buffer =\n        match download_extension_tar(remote_ext_base_url, &ext_path.to_string()).await {\n            Ok(buffer) => buffer,\n            Err(error_message) => {\n                return Err(anyhow::anyhow!(\n                    \"error downloading extension {:?}: {:?}\",\n                    ext_name,\n                    error_message\n                ));\n            }\n        };\n\n    let download_size = download_buffer.len() as u64;\n    info!(\"Download size {:?}\", download_size);\n    // it's unclear whether it is more performant to decompress into memory or not\n    // TODO: decompressing into memory can be avoided\n    let decoder = Decoder::new(download_buffer.as_ref())?;\n    let mut archive = Archive::new(decoder);\n\n    let unzip_dest = pgbin\n        .strip_suffix(\"/bin/postgres\")\n        .expect(\"bad pgbin\")\n        .to_string()","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/compute_tools/src/extension_server.rs#L140-L176","documentation":"The extension download helper fetched {base_url}/{ext_path} via download_extension_tar and the HTTP request failed; the error wraps the transfer error verbatim (DNS failure, connect refused, 404 for a wrong archive path, timeout). There is deliberately no retry yet (see the TODO in the source), so a single transient network hiccup surfaces as a hard error for that extension.","triggerScenarios":"download_extension() called with a remote_ext_base_url that is unreachable from the compute, or an ext_path that does not exist on the bucket (404), or the URL join in download_extension_tar produced a malformed URI (bad base/path combination), or the transfer timed out.","commonSituations":"Wrong extension archive naming (version/platform mismatch, missing ext/ prefix); air-gapped computes without egress to the bucket; DNS/veth issues in the sandbox; transient S3 outage with no retry to absorb it; base URL typo from --remote-ext-base-url.","solutions":["Check the wrapped error: a 404 means the archive path is wrong - compare the requested ext_path against the bucket layout","Verify egress/DNS from the compute pod to remote_ext_base_url (curl the full URL)","Fix the extension version/name so it maps to an archive that exists in the bucket","Retry the request or restart the endpoint for transient network failures; better, add retry/backoff around download_extension_tar (the source marks it TODO)"],"exampleFix":"// before (compute_tools/src/extension_server.rs) - no retry\nlet download_buffer = match download_extension_tar(remote_ext_base_url, &ext_path.to_string()).await { ... };\n// after: bounded retry for transient failures\nlet download_buffer = retry(3, Duration::from_secs(2), || async {\n    download_extension_tar(remote_ext_base_url, &ext_path.to_string()).await\n}).await.map_err(|e| anyhow!(\"error downloading extension {ext_name:?}: {e:?}\"))?;","handlingStrategy":"retry","validationCode":"// Verify the exact artifact URL before download_extension runs\nlet url = remote_ext_base_url.join(&ext_path.to_string())\n    .with_context(|| format!(\"bad ext url {ext_path} base {remote_ext_base_url}\"))?;\nif reqwest::get(url.clone()).await?.status() == 404 { anyhow::bail!(\"no such extension archive: {url}\"); }","typeGuard":"fn extension_url_resolvable(base: &Url, ext_path: &str) -> bool {\n    base.join(ext_path).is_ok()\n}","tryCatchPattern":"// Retry transient transfer errors a few times before failing the extension install\nfor attempt in 0..3 {\n    match download_extension_tar(remote_ext_base_url, &ext_path.to_string()).await {\n        Ok(buf) => return Ok(buf),\n        Err(e) if attempt == 2 => return Err(anyhow!(\"error downloading extension {ext_name:?}: {e:?}\")),\n        Err(_) => tokio::time::sleep(Duration::from_secs(2u64.pow(attempt as u32))).await,\n    }\n}","preventionTips":["Pre-check egress/DNS from the compute subnet to the extension bucket","Keep extension archive naming deterministic (ext/<name>.tar.zst) and covered by tests","Add the retry/backoff the source TODO asks for so one timeout does not fail an extension install"],"tags":["rust","compute-ctl","extensions","http","download","network"],"backgroundTag":"http-download-failed","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}