{"record":{"id":"2c4003f237009bdc","repo":"siyuan-note/siyuan","slug":"url-must-start-with-http-or-https","errorCode":null,"errorMessage":"URL must start with http:// or https://","messagePattern":"URL must start with http:// or https://","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/httprequest.go","lineNumber":63,"sourceCode":"\t\treturn errors.New(\"failed to resolve host: \" + err.Error())\n\t}\n\tfor _, ip := range ips {\n\t\t// 与 SSRFSafeDialer 共用 isPrivateIP，覆盖 NAT64、6to4、Teredo 等 IPv6 过渡地址。\n\t\tif isPrivateIP(ip) {\n\t\t\treturn errors.New(\"access to private/internal IP is prohibited\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// HTTPRequest 发起一次通用 HTTP 调用，供智能体 http_request 工具使用。\n// 与 WebFetch 不同：本函数不做 HTML→Markdown 转换，文本类响应（含 JSON/XML）原样返回，\n// 便于智能体直接消费 REST API 的 JSON 输出。method 取值：GET/POST/PUT/DELETE/PATCH。\n// 返回的 text 为响应正文（文本类）或落盘后的文件路径（二进制类）。\nfunc HTTPRequest(method, rawURL string, headers map[string]string, body string) (statusCode int, contentType string, text string, err error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil || (u.Scheme != \"http\" && u.Scheme != \"https\") {\n\t\treturn 0, \"\", \"\", errors.New(\"URL must start with http:// or https://\")\n\t}\n\tif u.Host == \"\" {\n\t\treturn 0, \"\", \"\", errors.New(\"URL has no host\")\n\t}\n\n\tif serr := CheckHostSSRF(u.Hostname()); serr != nil {\n\t\treturn 0, \"\", \"\", serr\n\t}\n\n\tmethod = strings.ToUpper(strings.TrimSpace(method))\n\tif method == \"\" {\n\t\tmethod = \"GET\"\n\t}\n\n\trequest := httpclient.NewBrowserRequest()\n\tfor k, v := range headers {\n\t\trequest.SetHeader(k, v)\n\t}","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/util/httprequest.go#L45-L81","documentation":"Returned by HTTPRequest when url.Parse fails for the input, or succeeds but yields a scheme that is neither http nor https. The check is the first validation in HTTPRequest and rejects mailto:, file:, ftp:, data:, and other schemes before any network activity.","triggerScenarios":"Calling util.HTTPRequest with a URL that is unparseable, has no scheme, or uses a non-http(s) scheme (file://, ftp://, mailto:, data:, javascript:, ws://).","commonSituations":"An agent passed a bare hostname ('example.com') without a scheme; a copy-paste included a file:// or mailto: link; the URL was constructed by string concatenation that dropped the scheme; a WebSocket ws:// URL fed to the HTTP tool.","solutions":["Prefix the URL with 'http://' or 'https://' before calling HTTPRequest.","If you accept user/agent input, normalize it with url.Parse and force/reject the scheme upstream.","Use a different code path for non-HTTP schemes (e.g. kernel file APIs for file://)."],"exampleFix":"// before\nutil.HTTPRequest(\"GET\", rawURL, nil, \"\")\n\n// after\nu, err := url.Parse(rawURL)\nif err != nil || (u.Scheme != \"http\" && u.Scheme != \"https\") {\n    if u != nil && u.Host != \"\" {\n        rawURL = \"https://\" + rawURL\n    } else {\n        return errors.New(\"a full http(s) URL is required\")\n    }\n}\nutil.HTTPRequest(\"GET\", rawURL, nil, \"\")","handlingStrategy":"validation","validationCode":"func validHTTPURL(raw string) bool {\n    u, err := url.Parse(raw)\n    return err == nil && (u.Scheme == \"http\" || u.Scheme == \"https\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Normalize agent/user URLs to force an http(s) scheme before calling HTTPRequest.","Reject mailto:, file:, ftp:, data: at the input boundary."],"tags":["network","validation","url","http-request"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}