siyuan-note/siyuan · error

read body failed:

Error message

read body failed: 

What it means

fetchBytes reads the download response body with io.ReadAll over an io.LimitReader. If the body stream fails mid-read (connection reset, timeout, truncated transfer), the error is wrapped as "read body failed: <underlying error>". The underlying transport error is appended so the real cause is visible.

Solutions

  1. Read the wrapped underlying error to identify the transport cause (reset vs timeout vs TLS)
  2. Retry the download; transient resets usually succeed on a second attempt
  3. Check proxy/VPN/firewall stability, especially for large archives
  4. Download on a stable connection or reduce the source size if it consistently truncates
Defensive patterns

Strategy: retry

Validate before calling

const resp = await fetch(url, {method: "HEAD"}); if (!resp.ok) throw new Error("source not reachable");

Try / catch

try { await installSkill(src); } catch (e) { if (/read body failed/.test(e.message)) { await retryWithBackoff(() => installSkill(src), 3); } else throw e; }

Prevention

When it happens

Trigger: Network interruption while streaming a skill zip from codeload.github.com or a raw URL: connection reset by peer, TLS handshake drop, context/proxy timeout, or malformed chunked encoding during the GET initiated by httpclient.NewBrowserRequest().Get.

Common situations: Unstable Wi-Fi or mobile connections; aggressive corporate proxies or firewalls killing long transfers; GitHub closing connections on very large archives; VPN drops mid-download.

Related errors


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

Appendix: source

Thrown at kernel/util/skill.go:710

	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
	}
	defer os.RemoveAll(tmpRoot)

	zipPath := filepath.Join(tmpRoot, "src.zip")
	if err := os.WriteFile(zipPath, data, 0644); err != nil {
		return nil, err

View on GitHub (pinned to 8641553a1f)