{"record":{"id":"a246c0ac69d48cf3","repo":"siyuan-note/siyuan","slug":"data-msg-data-message-window-siyuan-langua","errorCode":null,"errorMessage":"data?.msg || data?.message || window.siyuan.languages._kernel[28]","messagePattern":"data\\?\\.msg \\|\\| data\\?\\.message \\|\\| window\\.siyuan\\.languages\\._kernel\\[28\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/ai/editorSSE.ts","lineNumber":110,"sourceCode":"    onEvent: (event: TAIEditorSSEEvent) => void,\n    signal: AbortSignal,\n) => {\n    const response = await fetch(\"/api/ai/editor/chat\", {\n        method: \"POST\",\n        headers: {\"Content-Type\": \"application/json\"},\n        body: JSON.stringify(request),\n        signal,\n    });\n    const contentType = response.headers.get(\"Content-Type\") || \"\";\n    if (!response.ok || !contentType.includes(\"text/event-stream\")) {\n        let message = window.siyuan.languages._kernel[28];\n        try {\n            const data = await response.json();\n            message = data?.msg || data?.message || message;\n        } catch (e) {\n            // 响应不是 JSON 时使用统一错误文案。\n        }\n        throw new Error(message);\n    }\n    const reader = response.body?.getReader();\n    if (!reader) {\n        throw new Error(window.siyuan.languages._kernel[28]);\n    }\n    const decoder = new TextDecoder();\n    const parserState = createAIEditorSSEParserState();\n    let terminalReceived = false;\n    while (true) {\n        const result = await reader.read();\n        if (result.done) {\n            break;\n        }\n        parseAIEditorSSE(parserState, decoder.decode(result.value, {stream: true})).forEach(event => {\n            terminalReceived = terminalReceived || event.type === \"done\" || event.type === \"error\";\n            onEvent(event);\n        });\n    }","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/app/src/ai/editorSSE.ts#L92-L128","documentation":"Thrown by util.WebFetch in kernel/util/webfetch.go when the URL parses successfully and carries an http/https scheme, but its host component is empty. This happens with malformed inputs such as \"http:///path\", \"https:/single-slash\", or \"http://?q=1\", which net/url.Parse accepts but which leave u.Host empty. The check exists because an empty host would otherwise send a request to the local machine, which is both useless and an SSRF risk.","triggerScenarios":"Calling util.WebFetch(rawURL, format) with a scheme-relative or single-slash URL like \"https:/example.com/a\" (parses with empty Host), \"http:///foo\", or a URL built by string concatenation where the host segment was accidentally dropped or contained unescaped characters that broke host parsing.","commonSituations":"Agent/AI features or plugins passing user-typed or LLM-generated URLs that were truncated or copy-pasted with a missing slash; URLs assembled via fmt.Sprintf where the host variable was empty; input like \"https://\" passed straight through from a chat prompt.","solutions":["Inspect the exact rawURL string passed to WebFetch; look for single-slash schemes (\"https:/...\") or a missing host segment","Normalize the URL before calling WebFetch: run url.Parse, require u.Hostname() != \"\", and repair common typos such as \"https:/\" -> \"https://\"","If the URL comes from an LLM or user input, validate it at the boundary and reject/repair empty-host URLs before it reaches the kernel","Check for stray whitespace or embedded control characters pasted into the URL"],"exampleFix":"// before\nresult, err := util.WebFetch(userURL, \"markdown\")\n\n// after\nu, perr := url.Parse(strings.TrimSpace(userURL))\nif perr != nil || u.Host == \"\" || (u.Scheme != \"http\" && u.Scheme != \"https\") {\n    return fmt.Errorf(\"invalid URL %q: must be a fully-qualified http(s) URL with a host\", userURL)\n}\nresult, err := util.WebFetch(u.String(), \"markdown\")","handlingStrategy":"validation","validationCode":"u, err := url.Parse(strings.TrimSpace(rawURL))\nif err != nil || (u.Scheme != \"http\" && u.Scheme != \"https\") || u.Host == \"\" {\n    return fmt.Errorf(\"reject URL %q before WebFetch: need http(s) scheme and non-empty host\", rawURL)\n}\nresult, err := util.WebFetch(u.String(), format)","typeGuard":"func isFetchableURL(rawURL string) bool {\n    u, err := url.Parse(strings.TrimSpace(rawURL))\n    return err == nil && (u.Scheme == \"http\" || u.Scheme == \"https\") && u.Host != \"\"\n}","tryCatchPattern":"null","preventionTips":["Never build URLs by bare string concatenation; always assemble with url.URL and set u.Host explicitly","Validate LLM- or user-supplied URLs at the input boundary with url.Parse plus a non-empty Host check","Trim whitespace and reject control characters before passing URLs to WebFetch"],"tags":["url","validation","webfetch","ssrf"],"backgroundTag":"invalid-url","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}