{"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/251596fc0de2f9528c00c224252fd073a99973f4/kernel/plugin/api_client.go#L46-L82","documentation":"Thrown by siyuan.client.fetch when the first argument (path) is a string but does not begin with '/'. fetch only accepts a server-relative path because it rewrites it to http://127.0.0.1:<port><path>; absolute or scheme-relative URLs are rejected to keep plugin requests on the local kernel.","triggerScenarios":"Calling fetch('api/foo') (missing leading slash), fetch('http://host/api/foo') (absolute URL), or fetch('//host/path') (protocol-relative). Any string passed as arg 0 that lacks a '/' prefix fails the strings.HasPrefix check at api_client.go:63.","commonSituations":"Plugin author copies a URL from browser devtools (which may keep a relative form), builds the path from a variable that was trimmed, or mistakes siyuan.client.fetch for the browser fetch (which accepts full URLs).","solutions":["Prefix the path with '/' so it is server-relative, e.g. '/api/query/similarBlocks'.","Normalize once: const p = path.startsWith('/') ? path : '/' + path.","Do not pass a full URL; fetch only targets the local kernel. Strip the origin/host before calling."],"exampleFix":"// before\nawait siyuan.client.fetch('api/query/similarBlocks', { method: 'POST', body: '{}' });\n// after\nawait siyuan.client.fetch('/api/query/similarBlocks', { method: 'POST', body: '{}' });","handlingStrategy":"validation","validationCode":"function safePath(p) {\n  if (typeof p !== 'string') throw new TypeError('path must be a string');\n  if (!p.startsWith('/')) throw new Error(\"path must start with /\");\n  return p;\n}\nawait siyuan.client.fetch(safePath(path));","typeGuard":"const isServerPath = (v) => typeof v === 'string' && v.startsWith('/');","tryCatchPattern":null,"preventionTips":["Always write kernel paths as '/api/...'.","Centralize endpoint constants so paths are never assembled ad hoc.","Remember siyuan.client.fetch is not the browser fetch — no origin allowed."],"tags":["client-fetch","validation","argument","path"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}