{"record":{"id":"f144a9894e40528e","repo":"astrid-runtime/astrid","slug":"release-asset-name-has-no-download-url","errorCode":null,"errorMessage":"release asset '{name}' has no download URL","messagePattern":"release asset '(.+?)' has no download URL","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/self_update/mod.rs","lineNumber":327,"sourceCode":"    anyhow::ensure!(\n        assets.len() <= MAX_RELEASE_ASSETS,\n        \"release contains too many assets\"\n    );\n    let mut matches = assets\n        .iter()\n        .filter(|asset| asset.get(\"name\").and_then(|value| value.as_str()) == Some(name));\n    let asset = matches\n        .next()\n        .ok_or_else(|| anyhow::anyhow!(\"release has no asset '{name}'\"))?;\n    anyhow::ensure!(\n        matches.next().is_none(),\n        \"release contains duplicate asset '{name}'\"\n    );\n    asset\n        .get(\"browser_download_url\")\n        .and_then(|value| value.as_str())\n        .filter(|url| !url.is_empty())\n        .ok_or_else(|| anyhow::anyhow!(\"release asset '{name}' has no download URL\"))\n}\n\nfn publisher_bundle_url<'a>(\n    release: &'a serde_json::Value,\n    archive_name: &str,\n) -> Result<&'a str, UpdateStageError> {\n    let bundle_name = format!(\"{archive_name}.sigstore.json\");\n    exact_asset_url(release, &bundle_name)\n        .map_err(|error| UpdateStageError::publisher(error.to_string()))\n}\n\nfn integrity_manifest_url(release: &serde_json::Value) -> Result<&str, UpdateStageError> {\n    exact_asset_url(release, \"BLAKE3SUMS.txt\")\n        .map_err(|error| UpdateStageError::integrity(error.to_string()))\n}\n\n/// Stream a URL into memory under the size cap.\npub(super) async fn download_bounded(","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/self_update/mod.rs#L309-L345","documentation":"The final step of exact_asset_url extracts the asset's browser_download_url, requiring a non-empty string. If the matched asset object lacks that field, has a non-string value, or an empty URL, it throws this error. It prevents proceeding with a download target that doesn't exist.","triggerScenarios":"A release asset entry in the fetched JSON has no browser_download_url — e.g. assets from a non-GitHub mirror, partially populated API responses, or empty-string URLs — after the name matched successfully.","commonSituations":"Custom update source (ASTRID_UPDATE_REPO) proxied through an API gateway that drops or renames fields; hand-rolled release JSON fixtures; GitHub API shape changes or partial responses.","solutions":["Confirm the release JSON asset objects include browser_download_url (curl the release endpoint and inspect).","Remove any proxy/mirror that strips fields, or fetch releases directly from api.github.com.","Re-run the release workflow so assets are published with complete metadata.","If a GitHub API change is suspected, upgrade the CLI to a version matching the current API shape."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// validate asset shape before consuming\nconst url = asset.browser_download_url;\nif (typeof url !== \"string\" || url.length === 0) throw new Error(\"asset missing download URL\");","typeGuard":"fn has_download_url(asset: &serde_json::Value) -> bool {\n    asset.get(\"browser_download_url\")\n        .and_then(|v| v.as_str())\n        .map(|s| !s.is_empty())\n        .unwrap_or(false)\n}","tryCatchPattern":null,"preventionTips":["Fetch releases directly from api.github.com, not through field-dropping proxies.","Validate release JSON shape once at fetch time before asset lookups."],"tags":["github","release","self-update","download","unexpected-response"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}