{"record":{"id":"def7cdf7e9534927","repo":"siyuan-note/siyuan","slug":"invalid-download-url-s","errorCode":null,"errorMessage":"invalid download URL: %s","messagePattern":"invalid download URL: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":676,"sourceCode":"\n\t// commit/<sha>\n\tif len(parts) >= 4 && parts[2] == \"commit\" {\n\t\tsha := parts[3]\n\t\treturn normalizedSkillSource{\n\t\t\tdownloadURL: \"https://codeload.github.com/\" + ownerRepo + \"/zip/\" + sha,\n\t\t\tisZip:       true,\n\t\t}, nil\n\t}\n\n\t// 纯仓库地址：默认 main，失败回退 master\n\treturn codeloadSource(ownerRepo, \"main\"), nil\n}\n\n// downloadSkillSource 下载 skill 源，返回字节、Content-Type\nfunc downloadSkillSource(src normalizedSkillSource) (data []byte, contentType string, err error) {\n\tu, perr := url.Parse(src.downloadURL)\n\tif perr != nil || u.Host == \"\" {\n\t\treturn nil, \"\", fmt.Errorf(\"invalid download URL: %s\", src.downloadURL)\n\t}\n\tif cerr := CheckHostSSRF(u.Hostname()); cerr != nil {\n\t\treturn nil, \"\", cerr\n\t}\n\n\tdata, contentType, err = fetchBytes(src.downloadURL)\n\tif err == nil {\n\t\treturn data, contentType, nil\n\t}\n\n\t// codeload main 分支 404 时回退 master\n\tif src.isZip && src.branch == \"main\" {\n\t\townerRepo := strings.TrimPrefix(strings.TrimPrefix(src.downloadURL, \"https://codeload.github.com/\"), \"http://codeload.github.com/\")\n\t\townerRepo = strings.TrimSuffix(ownerRepo, \"/zip/refs/heads/main\")\n\t\tfallback := codeloadSource(ownerRepo, \"master\")\n\t\tdata, contentType, ferr := fetchBytes(fallback.downloadURL)\n\t\tif ferr != nil {\n\t\t\treturn nil, \"\", fmt.Errorf(\"download failed (tried main and master): %v\", err)","sourceCodeStart":658,"sourceCodeEnd":694,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill.go#L658-L694","documentation":"downloadSkillSource validates the normalized source's download URL before fetching: url.Parse must succeed and the URL must have a host. This is an internal consistency check on the URL produced by source normalization, so hitting it usually means the normalized skill source was constructed with a malformed or host-less URL (or the caller supplied an empty one).","triggerScenarios":"InstallSkill reaches downloadSkillSource with a normalizedSkillSource whose downloadURL fails url.Parse or has an empty Host, e.g. an empty downloadURL, a scheme-relative URL like //host/path, or a URL with control characters that break parsing.","commonSituations":"Programmatic callers constructing normalizedSkillSource directly with an empty or hand-built downloadURL; source strings containing whitespace or newline characters; a custom direct-link source that is not actually a valid absolute URL.","solutions":["Pass a complete absolute URL (scheme + host + path) as the skill source, e.g. https://example.com/skill.zip","Trim whitespace/newlines from the source string before calling InstallSkill","If constructing normalizedSkillSource yourself, verify downloadURL parses and has a Host before calling downloadSkillSource","Use one of the supported source forms: github.com repo/tree/commit/release URL, raw.githubusercontent.com URL, or a full direct-link URL"],"exampleFix":"// before\nsrc := normalizedSkillSource{downloadURL: \"example.com/skill.zip\"} // no scheme -> Host ok? actually parse fails host\n// after\nsrc := normalizedSkillSource{downloadURL: \"https://example.com/skill.zip\"}","handlingStrategy":"validation","validationCode":"function isAbsoluteHttpUrl(s) {\n  try {\n    const u = new URL(s.trim())\n    return (u.protocol === \"http:\" || u.protocol === \"https:\") && u.hostname !== \"\"\n  } catch { return false }\n}\nif (!isAbsoluteHttpUrl(src)) throw new Error(\"skill source must be an absolute http(s) URL\")","typeGuard":"const isNonEmptyUrl = (s) => typeof s === \"string\" && s.trim().length > 0 && /^https?:\\/\\//.test(s.trim())","tryCatchPattern":"try {\n  await installSkill(src)\n} catch (e) {\n  if (String(e).startsWith(\"invalid download URL\")) {\n    logError(\"normalized download URL malformed\", { src })\n  }\n}","preventionTips":["Trim whitespace and newlines from source strings before passing them in","Never build download URLs by raw string concatenation without validating the result","Only construct skill sources via the supported URL forms (GitHub, raw, direct link)"],"tags":["url","download","validation"],"backgroundTag":"invalid-url-format","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}