{"record":{"id":"886561280f49dc3f","repo":"siyuan-note/siyuan","slug":"path-must-start-with","errorCode":null,"errorMessage":"path must start with /","messagePattern":"path must start with /","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/plugin/api_client.go","lineNumber":64,"sourceCode":"\tclient := rt.NewObject()\n\n\tlo.Must0(client.Set(\"fetch\", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {\n\t\tpromise, resolve, reject := rt.NewPromise()\n\n\t\tvar argErr error\n\t\tvar path string\n\t\tmethod := \"GET\"\n\t\theaders := map[string]string{}\n\t\tvar bodyString *string\n\t\tvar bodyBytes *[]byte\n\n\t\tif goja.IsString(call.Argument(0)) {\n\t\t\tpath = call.Argument(0).String()\n\t\t} else {\n\t\t\targErr = fmt.Errorf(\"path required\")\n\t\t}\n\t\tif argErr == nil && !strings.HasPrefix(path, \"/\") {\n\t\t\targErr = fmt.Errorf(\"path must start with /\")\n\t\t}\n\t\tif argErr == nil {\n\t\t\tif init := call.Argument(1); isJsValueNotNull(init) {\n\t\t\t\tif initObj := init.ToObject(rt); initObj != nil {\n\t\t\t\t\tif m := initObj.Get(\"method\"); goja.IsString(m) {\n\t\t\t\t\t\tmethod = m.String()\n\t\t\t\t\t}\n\n\t\t\t\t\tif h := initObj.Get(\"headers\"); isJsValueNotNull(h) {\n\t\t\t\t\t\tif exportErr := rt.ExportTo(h, &headers); exportErr != nil {\n\t\t\t\t\t\t\targErr = fmt.Errorf(\"failed to export headers: %w\", exportErr)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif argErr == nil {\n\t\t\t\t\t\tif b := initObj.Get(\"body\"); isJsValueNotNull(b) {\n\t\t\t\t\t\t\tif goja.IsString(b) {\n\t\t\t\t\t\t\t\tbodyString = new(b.String())","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/plugin/api_client.go#L46-L82","documentation":"The fetch bridge validates that the request path starts with \"/\" — only kernel-relative API paths are allowed, not absolute URLs. A path without the leading slash is rejected so plugins cannot point the client at arbitrary hosts or malformed endpoints.","triggerScenarios":"Calling siyuan.client.fetch(\"api/notebook/lsNotebooks\") (missing leading slash), or passing a full URL like \"http://127.0.0.1:6806/api/...\" or \"https://...\".","commonSituations":"Copying absolute URLs from API documentation or curl examples directly into the plugin client; building paths from config that dropped the leading slash.","solutions":["Prefix the path with \"/\": fetch(\"/api/...\")","Strip the origin from full URLs: new URL(full).pathname","Do not use absolute URLs — the client always targets the local kernel","Add a JS-side guard: if (!p.startsWith(\"/\")) p = \"/\" + p"],"exampleFix":"// before\nawait siyuan.client.fetch(\"http://127.0.0.1:6806/api/notebook/lsNotebooks\")\n\n// after\nawait siyuan.client.fetch(\"/api/notebook/lsNotebooks\")","handlingStrategy":"validation","validationCode":"function toKernelPath(p) {\n  if (typeof p !== \"string\") throw new TypeError(\"path must be a string\");\n  if (/^https?:\\/\\//.test(p)) return new URL(p).pathname;\n  return p.startsWith(\"/\") ? p : \"/\" + p;\n}","typeGuard":"const isRelativeApiPath = (v) => typeof v === \"string\" && v.startsWith(\"/\");","tryCatchPattern":"try {\n  const res = await siyuan.client.fetch(path, opts);\n} catch (e) {\n  if (String(e).includes(\"path must start with /\")) {\n    console.error(\"Use a kernel-relative path like /api/...\", path);\n  }\n}","preventionTips":["Strip origins from copied curl/doc URLs with new URL(u).pathname","Never pass absolute URLs to siyuan.client.fetch","Centralize all kernel calls in one helper that normalizes paths"],"tags":["plugin","api","fetch","url"],"backgroundTag":"invalid-url-format","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"}