siyuan-note/siyuan · error
invalid download URL
Error message
invalid download URL: %s
What it means
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).
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
Example fix
// before
src := normalizedSkillSource{downloadURL: "example.com/skill.zip"} // no scheme -> Host ok? actually parse fails host
// after
src := normalizedSkillSource{downloadURL: "https://example.com/skill.zip"} Defensive patterns
Strategy: validation
Validate before calling
function isAbsoluteHttpUrl(s) {
try {
const u = new URL(s.trim())
return (u.protocol === "http:" || u.protocol === "https:") && u.hostname !== ""
} catch { return false }
}
if (!isAbsoluteHttpUrl(src)) throw new Error("skill source must be an absolute http(s) URL") Type guard
const isNonEmptyUrl = (s) => typeof s === "string" && s.trim().length > 0 && /^https?:\/\//.test(s.trim())
Try / catch
try {
await installSkill(src)
} catch (e) {
if (String(e).startsWith("invalid download URL")) {
logError("normalized download URL malformed", { src })
}
} Prevention
- 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)
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- invalid custom emoji URL
- CalDAV: calendar object path is invalid
- CalDAV: calendar path is invalid
- CardDAV: address book path is invalid
- CardDAV: path is invalid
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/def7cdf7e9534927.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/util/skill.go:676
// commit/<sha>
if len(parts) >= 4 && parts[2] == "commit" {
sha := parts[3]
return normalizedSkillSource{
downloadURL: "https://codeload.github.com/" + ownerRepo + "/zip/" + sha,
isZip: true,
}, nil
}
// 纯仓库地址:默认 main,失败回退 master
return codeloadSource(ownerRepo, "main"), nil
}
// downloadSkillSource 下载 skill 源,返回字节、Content-Type
func downloadSkillSource(src normalizedSkillSource) (data []byte, contentType string, err error) {
u, perr := url.Parse(src.downloadURL)
if perr != nil || u.Host == "" {
return nil, "", fmt.Errorf("invalid download URL: %s", src.downloadURL)
}
if cerr := CheckHostSSRF(u.Hostname()); cerr != nil {
return nil, "", cerr
}
data, contentType, err = fetchBytes(src.downloadURL)
if err == nil {
return data, contentType, nil
}
// codeload main 分支 404 时回退 master
if src.isZip && src.branch == "main" {
ownerRepo := strings.TrimPrefix(strings.TrimPrefix(src.downloadURL, "https://codeload.github.com/"), "http://codeload.github.com/")
ownerRepo = strings.TrimSuffix(ownerRepo, "/zip/refs/heads/main")
fallback := codeloadSource(ownerRepo, "master")
data, contentType, ferr := fetchBytes(fallback.downloadURL)
if ferr != nil {
return nil, "", fmt.Errorf("download failed (tried main and master): %v", err)View on GitHub (pinned to 9f775e8a12)