siyuan-note/siyuan · error

download failed: HTTP

Error message

download failed: HTTP %d

What it means

fetchBytes performs a size-limited GET for skill source downloads (e.g. GitHub codeload zips or raw SKILL.md). When the HTTP response status is >= 400 (client or server error), it aborts with "download failed: HTTP <status>" instead of reading the body. This is a guard against processing error responses as installable content.

Solutions

  1. Verify the source URL is reachable in a browser or with curl -I and returns 200
  2. Check the repo exists, is public, and its default branch is main or master
  3. Wait out GitHub rate limits or use an authenticated environment if rate-limited (429/403)
  4. Inspect the numeric HTTP status in the message to distinguish 404 (wrong source) from 403/429 (auth/rate limit) from 5xx (server problem)
Defensive patterns

Strategy: try-catch

Validate before calling

const u = new URL(src); if (!/codeload\.github\.com$|^raw\./.test(u.hostname) || !u.pathname) throw new Error("bad skill source URL");

Type guard

function isHttpOk(status) { return typeof status === "number" && status >= 200 && status < 400; }

Try / catch

try { const data = await installSkill(src); } catch (e) { if (/download failed: HTTP (4\d\d)/.test(e.message)) { const code = +e.message.match(/HTTP (\d+)/)[1]; if (code === 404) checkRepoExists(); else if (code === 429) backoffAndRetry(); } else throw e; }

Prevention

When it happens

Trigger: Calling downloadSkillSource -> fetchBytes with a URL that returns a 4xx/5xx status: nonexistent GitHub repo or branch (404), private repo without credentials (404/403), rate-limited request (403/429), or server-side error (5xx). For codeload main-branch zips a fallback to master is attempted before this surfaces.

Common situations: Typo in repo owner/name; installing from a private or deleted repository; GitHub API/codeload rate limiting on anonymous requests; default branch renamed so neither main nor master exists; corporate proxy blocking codeload.github.com.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/5fc586aa309a52e9. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/skill.go:704

		data, contentType, ferr := fetchBytes(fallback.downloadURL)
		if ferr != nil {
			return nil, "", fmt.Errorf("download failed (tried main and master): %v", err)
		}
		return data, contentType, nil
	}
	return nil, "", err
}

// fetchBytes 执行带大小限制的 GET
func fetchBytes(rawURL string) (data []byte, contentType string, err error) {
	resp, err := httpclient.NewBrowserRequest().Get(rawURL)
	if err != nil {
		return nil, "", errors.New("download failed: " + err.Error())
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		return nil, "", fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
	}

	contentType = resp.Header.Get("Content-Type")
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxSkillDownloadBytes+1))
	if err != nil {
		return nil, "", errors.New("read body failed: " + err.Error())
	}
	if len(body) > maxSkillDownloadBytes {
		return nil, "", errors.New("skill source too large (limit 10MB)")
	}
	return body, contentType, nil
}

// installFromZip 解压 zip 并安装其中的 skill
func installFromZip(data []byte) (*InstallSkillResult, error) {
	tmpRoot := filepath.Join(TempDir, "ai", "skill-install", gulu.Rand.String(7))
	if err := os.MkdirAll(tmpRoot, 0755); err != nil {
		return nil, err

View on GitHub (pinned to 8641553a1f)