{"record":{"id":"af460e97ddff4237","repo":"router-for-me/CLIProxyAPI","slug":"asset-q-missing-download-url","errorCode":null,"errorMessage":"asset %q missing download url","messagePattern":"asset %q missing download url","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/pluginstore/github.go","lineNumber":129,"sourceCode":"// leading \"v\"/\"V\" and validating the result.\nfunc ReleaseVersion(release Release) (string, error) {\n\tversion := normalizeVersion(release.TagName)\n\tif !validPluginVersion(version) {\n\t\treturn \"\", fmt.Errorf(\"invalid release tag %q\", release.TagName)\n\t}\n\treturn version, nil\n}\n\nfunc (c Client) DownloadAsset(ctx context.Context, asset ReleaseAsset) ([]byte, error) {\n\tdownloadURL := strings.TrimSpace(asset.BrowserDownloadURL)\n\tapiURL := strings.TrimSpace(asset.APIURL)\n\tif downloadURL == \"\" || c.releaseAssetAPIAuthenticated(apiURL) {\n\t\tif apiURL != \"\" {\n\t\t\tdownloadURL = apiURL\n\t\t}\n\t}\n\tif downloadURL == \"\" {\n\t\treturn nil, fmt.Errorf(\"asset %q missing download url\", asset.Name)\n\t}\n\treturn c.get(ctx, downloadURL, \"application/octet-stream\", RequestKindArtifact, 0)\n}\n\nfunc (c Client) releaseAssetAPIAuthenticated(apiURL string) bool {\n\tapiURL = strings.TrimSpace(apiURL)\n\tif apiURL == \"\" {\n\t\treturn false\n\t}\n\tif item, ok := matchingResolvedAuthConfig(c.ResolvedAuth, apiURL, RequestKindArtifact); ok {\n\t\treturn resolvedAuthConfigured(item)\n\t}\n\treturn AuthConfigured(c.Auth, apiURL, RequestKindArtifact)\n}\n\nfunc (c Client) get(ctx context.Context, requestURL string, accept string, kind string, maxSize int64) ([]byte, error) {\n\tcurrentURL := strings.TrimSpace(requestURL)\n\tfor redirects := 0; ; redirects++ {","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/github.go#L111-L147","documentation":"Client.DownloadAsset picks a download URL for a GitHub release asset: it prefers asset.BrowserDownloadURL, but switches to asset.APIURL when the browser URL is empty or when releaseAssetAPIAuthenticated determines the API URL has configured auth (private repos need the API endpoint with token headers). 'asset %q missing download url' is thrown when both candidate URLs are empty/whitespace after that resolution.","triggerScenarios":"A ReleaseAsset JSON object from GitHub lacking both browser_download_url and url fields — either a hand-constructed Release, a filtered/rewritten API response, or an asset object of an unexpected kind (e.g. a data object where a source/browser URL was expected).","commonSituations":"Unit tests or fixtures building ReleaseAsset literals without URLs; API responses parsed into the wrong struct; processing a Release where the assets array was repackaged by a proxy or mirror that drops URL fields.","solutions":["Log asset.Name and the raw API response to see which fields are absent on the failing asset.","If you construct ReleaseAsset yourself (tests/tools), populate BrowserDownloadURL (from the release payload) before calling DownloadAsset.","If GitHub responses genuinely lack the fields, re-fetch the release to get fresh asset objects."],"exampleFix":"// before\nfor _, asset := range release.Assets {\n    data, err := client.DownloadAsset(ctx, asset) // fixture without URLs -> error\n}\n\n// after\nfor _, asset := range release.Assets {\n    if strings.TrimSpace(asset.BrowserDownloadURL) == \"\" && strings.TrimSpace(asset.APIURL) == \"\" {\n        continue // skip assets with no downloadable URL\n    }\n    data, err := client.DownloadAsset(ctx, asset)\n}","handlingStrategy":"validation","validationCode":"for _, asset := range release.Assets {\n    if strings.TrimSpace(asset.BrowserDownloadURL) == \"\" && strings.TrimSpace(asset.APIURL) == \"\" {\n        continue // skip non-downloadable entries instead of erroring mid-loop\n    }\n}","typeGuard":"func assetDownloadable(a pluginstore.ReleaseAsset) bool {\n    return strings.TrimSpace(a.BrowserDownloadURL) != \"\" || strings.TrimSpace(a.APIURL) != \"\"\n}","tryCatchPattern":null,"preventionTips":["Filter assets by platform and URL presence before downloading.","When building Release fixtures in tests, always set BrowserDownloadURL."],"tags":["github-api","release-asset","download-url","validation","plugin-store"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}