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

  1. Pass a complete absolute URL (scheme + host + path) as the skill source, e.g. https://example.com/skill.zip
  2. Trim whitespace/newlines from the source string before calling InstallSkill
  3. If constructing normalizedSkillSource yourself, verify downloadURL parses and has a Host before calling downloadSkillSource
  4. 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

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


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)