siyuan-note/siyuan · error

invalid github URL

Error message

invalid github URL: %s

What it means

normalizeGitHubURL rejects a github.com URL whose path does not contain at least an owner and a repository segment. The library parses the URL path into slash-separated parts and needs at least 2 parts to build the owner/repo pair it downloads from. Any github.com URL without owner/repo (e.g. bare github.com or github.com/owner) is invalid.

Solutions

  1. Provide a full github.com URL containing both owner and repository, e.g. https://github.com/owner/repo
  2. Check for stray slashes or truncation when pasting the URL; remove empty segments (a leading '//' produces empty parts)
  3. If pointing at a single file, use the full blob URL (https://github.com/owner/repo/blob/<branch>/<path>) or a raw.githubusercontent.com URL
  4. If using a non-GitHub source, pass its direct URL (raw.githubusercontent.com or any direct link) instead of a bare host

Example fix

// before
InstallSkill("https://github.com/siyuan-note")
// after
InstallSkill("https://github.com/siyuan-note/siyuan")
Defensive patterns

Strategy: validation

Validate before calling

const u = new URL(src)
if (u.hostname !== "github.com") throw new Error("not a github.com URL")
const parts = u.pathname.replace(/^\/+/, "").split("/").filter(Boolean)
if (parts.length < 2) throw new Error("github URL needs /<owner>/<repo>")

Type guard

function isGitHubRepoUrl(src) {
  try {
    const u = new URL(src)
    return u.hostname === "github.com" &&
      u.pathname.replace(/^\/+/, "").split("/").filter(Boolean).length >= 2
  } catch { return false }
}

Try / catch

try {
  await installSkill(src)
} catch (e) {
  if (String(e).includes("invalid github URL")) {
    showHint("Use a full URL: https://github.com/<owner>/<repo>")
  }
}

Prevention

When it happens

Trigger: Calling InstallSkill with a github.com URL whose path splits into fewer than 2 segments, e.g. https://github.com, https://github.com/, or https://github.com/owner (missing the repo name).

Common situations: Truncated copy-paste of a repo URL; stripping the repo segment when templating URLs; typo like https://github.com//repo producing empty parts; pasting a GitHub org page or search URL instead of a repo 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/4b4cbadba6a07e32. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/skill.go:635

// codeloadSource 构造 codeload zip 下载源,branch 用于 main→master 回退
func codeloadSource(ownerRepo, branch string) normalizedSkillSource {
	return normalizedSkillSource{
		downloadURL: "https://codeload.github.com/" + ownerRepo + "/zip/refs/heads/" + branch,
		isZip:       true,
		branch:      branch,
	}
}

// normalizeGitHubURL 处理 github.com 的各种路径形态
func normalizeGitHubURL(u *url.URL) (normalizedSkillSource, error) {
	// /owner/repo/tree/<branch> 或 /owner/repo/tree/<branch>/<path>
	// /owner/repo/commit/<sha>
	// /owner/repo/releases/download/<tag>/<asset>
	// /owner/repo(默认分支)
	parts := strings.Split(strings.Trim(u.Path, "/"), "/")
	if len(parts) < 2 {
		return normalizedSkillSource{}, fmt.Errorf("invalid github URL: %s", u.String())
	}
	ownerRepo := parts[0] + "/" + parts[1]

	// releases/download/<tag>/<asset>
	if len(parts) >= 6 && parts[2] == "releases" && parts[3] == "download" {
		asset := parts[5]
		// 是否 zip 交由 Content-Type 最终判定,这里仅按 asset 后缀预判
		return normalizedSkillSource{downloadURL: u.String(), isZip: strings.HasSuffix(asset, ".zip")}, nil
	}

	// tree/<branch>[/path] 或 blob/<branch>/...
	if len(parts) >= 4 && (parts[2] == "tree" || parts[2] == "blob") {
		branch := parts[3]
		if parts[2] == "blob" {
			// blob 指向单个文件,走 raw
			rawPath := strings.Join(parts[4:], "/")
			return normalizedSkillSource{
				downloadURL: "https://raw.githubusercontent.com/" + ownerRepo + "/" + branch + "/" + rawPath,

View on GitHub (pinned to 9f775e8a12)