{"record":{"id":"c21eba83c1877ce5","repo":"siyuan-note/siyuan","slug":"read-body-failed-c21eba","errorCode":null,"errorMessage":"read body failed: ","messagePattern":"read body failed: ","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":710,"sourceCode":"\treturn nil, \"\", err\n}\n\n// fetchBytes 执行带大小限制的 GET\nfunc fetchBytes(rawURL string) (data []byte, contentType string, err error) {\n\tresp, err := httpclient.NewBrowserRequest().Get(rawURL)\n\tif err != nil {\n\t\treturn nil, \"\", errors.New(\"download failed: \" + err.Error())\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, \"\", fmt.Errorf(\"download failed: HTTP %d\", resp.StatusCode)\n\t}\n\n\tcontentType = resp.Header.Get(\"Content-Type\")\n\tbody, err := io.ReadAll(io.LimitReader(resp.Body, maxSkillDownloadBytes+1))\n\tif err != nil {\n\t\treturn nil, \"\", errors.New(\"read body failed: \" + err.Error())\n\t}\n\tif len(body) > maxSkillDownloadBytes {\n\t\treturn nil, \"\", errors.New(\"skill source too large (limit 10MB)\")\n\t}\n\treturn body, contentType, nil\n}\n\n// installFromZip 解压 zip 并安装其中的 skill\nfunc installFromZip(data []byte) (*InstallSkillResult, error) {\n\ttmpRoot := filepath.Join(TempDir, \"ai\", \"skill-install\", gulu.Rand.String(7))\n\tif err := os.MkdirAll(tmpRoot, 0755); err != nil {\n\t\treturn nil, err\n\t}\n\tdefer os.RemoveAll(tmpRoot)\n\n\tzipPath := filepath.Join(tmpRoot, \"src.zip\")\n\tif err := os.WriteFile(zipPath, data, 0644); err != nil {\n\t\treturn nil, err","sourceCodeStart":692,"sourceCodeEnd":728,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/util/skill.go#L692-L728","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the wrapped underlying error to identify the transport cause (reset vs timeout vs TLS)","Retry the download; transient resets usually succeed on a second attempt","Check proxy/VPN/firewall stability, especially for large archives","Download on a stable connection or reduce the source size if it consistently truncates"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const resp = await fetch(url, {method: \"HEAD\"}); if (!resp.ok) throw new Error(\"source not reachable\");","typeGuard":null,"tryCatchPattern":"try { await installSkill(src); } catch (e) { if (/read body failed/.test(e.message)) { await retryWithBackoff(() => installSkill(src), 3); } else throw e; }","preventionTips":["Download over stable connections; avoid flaky Wi-Fi/VPN for large archives","Retry transient network failures with exponential backoff","Check proxy/firewall stability if truncation repeats on the same source","Keep sources small so transfer windows are short"],"tags":["network","io","download","connection"],"backgroundTag":"network-request-failed","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}