{"record":{"id":"31b104f071aaed86","repo":"siyuan-note/siyuan","slug":"fetch-failed-s","errorCode":null,"errorMessage":"fetch failed: %s","messagePattern":"fetch failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/webfetch.go","lineNumber":55,"sourceCode":"\tmaxWebFetchChars     = 50000\n)\n\nfunc WebFetch(rawURL, format string) (string, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil || (u.Scheme != \"http\" && u.Scheme != \"https\") {\n\t\treturn \"\", errors.New(\"URL must start with http:// or https://\")\n\t}\n\tif u.Host == \"\" {\n\t\treturn \"\", errors.New(\"URL has no host\")\n\t}\n\n\tif err := CheckHostSSRF(u.Hostname()); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := ssrfSafeClient.Get(rawURL)\n\tif err != nil {\n\t\treturn \"\", errors.New(\"fetch failed: \" + err.Error())\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\treturn \"\", fmt.Errorf(\"HTTP %d\", resp.StatusCode)\n\t}\n\n\tcontentType := resp.Header.Get(\"Content-Type\")\n\tmaxReadBytes := int64(maxWebFetchBytes)\n\tif !strings.HasPrefix(contentType, \"text/html\") && !strings.HasPrefix(contentType, \"text/plain\") {\n\t\tmaxReadBytes = maxWebFetchFileBytes\n\t}\n\tif resp.ContentLength > maxReadBytes {\n\t\treturn \"\", errors.New(\"response too large\")\n\t}\n\n\tbody, err := io.ReadAll(io.LimitReader(resp.Body, maxReadBytes))\n\tif err != nil {","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/webfetch.go#L37-L73","documentation":"After SSRF validation passes, WebFetch performs ssrfSafeClient.Get(rawURL); any transport-level failure (DNS resolution, TCP connect, TLS handshake, timeout) is wrapped into this error with the underlying net/http message appended. It means the kernel could not complete the HTTP request at all — no HTTP status was received.","triggerScenarios":"Host does not resolve (DNS failure), connection refused/timed out, TLS certificate errors, proxy failures, or the SSRF-safe client's dial policy blocking the connection (private IP re-check at connect time).","commonSituations":"Fetching an intranet hostname from a machine without VPN access; expired or self-signed certificates; the target site is down; corporate proxy not configured; DNS blocked in sandboxed environments.","solutions":["Read the wrapped cause after 'fetch failed: ' and fix accordingly (DNS, timeout, TLS)","Verify the host resolves and is reachable: curl -v the same URL from the kernel host","Check whether the URL points at a private/loopback address the SSRF-safe client refuses","Increase tolerance for slow sites by checking client timeouts; retry transient network failures"],"exampleFix":"// before\n_, err := WebFetch(target, \"markdown\") // opaque failure\n\n// after\n_, err := WebFetch(target, \"markdown\")\nif err != nil && strings.HasPrefix(err.Error(), \"fetch failed: \") {\n    log.Printf(\"transport error for %s: %v\", target, err) // inspect wrapped cause\n}","handlingStrategy":"retry","validationCode":"// Pre-check reachability cheaply\nif net.ParseIP(host) != nil && isPrivateIP(host) {\n    return errors.New(\"target is a private address; will be refused\")\n}","typeGuard":null,"tryCatchPattern":"content, err := WebFetch(url, format)\nif err != nil && strings.HasPrefix(err.Error(), \"fetch failed: \") {\n    // transport-level failure; inspect wrapped cause, retry with backoff for timeouts/resets\n}","preventionTips":["Confirm the target resolves from the kernel host before blaming WebFetch","Keep an eye on TLS certificate expiry for internal hosts","Retry transient transport errors with exponential backoff","Expect SSRF-safe dialing to refuse private/loopback targets"],"tags":["network","http","dns"],"backgroundTag":"http-request-failed","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}