{"record":{"id":"84a1a0b65752fdbe","repo":"siyuan-note/siyuan","slug":"local-storage-value-for-key-s-must-not-be-empty","errorCode":null,"errorMessage":"local storage value for key [%s] must not be empty","messagePattern":"local storage value for key \\[(.+?)\\] must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/storage.go","lineNumber":61,"sourceCode":"\tdefer localStorageLock.Unlock()\n\treturn getLocalStorage()\n}\n\nfunc SetLocalStorage(val map[string]any) (err error) {\n\tlocalStorageLock.Lock()\n\tdefer localStorageLock.Unlock()\n\treturn setLocalStorage(val)\n}\n\nfunc SetLocalStorageVals(keyVals map[string]any) (setKeyVals map[string]any, err error) {\n\tlocalStorageLock.Lock()\n\tdefer localStorageLock.Unlock()\n\n\tsetKeyVals = make(map[string]any, len(keyVals))\n\tlocalStorage := getLocalStorage()\n\tfor k, v := range keyVals {\n\t\tif v == nil {\n\t\t\terr = fmt.Errorf(\"local storage value for key [%s] must not be empty\", k)\n\t\t\treturn\n\t\t}\n\t\tlocalStorage[k] = v\n\t\tsetKeyVals[k] = v\n\t}\n\terr = setLocalStorage(localStorage)\n\treturn\n}\n\nfunc RemoveLocalStorageVals(keys []string) (err error) {\n\tlocalStorageLock.Lock()\n\tdefer localStorageLock.Unlock()\n\n\tlocalStorage := getLocalStorage()\n\tfor _, key := range keys {\n\t\tdelete(localStorage, key)\n\t}\n\treturn setLocalStorage(localStorage)","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/storage.go#L43-L79","documentation":"SetLocalStorageVals rejects any entry whose value is nil with the message 'local storage value for key [%s] must not be empty'. Local storage is a free-form key/value map persisted in the workspace, and the kernel treats a nil value as programmer error rather than an implicit delete (deletion goes through RemoveLocalStorageVals). The error includes the offending key for diagnostics.","triggerScenarios":"POST /api/storage/setLocalStorageVals with a JSON body whose map contains a key mapped to null. POST /api/storage/setLocalStorageVal (single) wraps the value into the same map path, so a null single value hits the same guard.","commonSituations":"A frontend/plugin stores a value that was just set to null by application logic (e.g. cleared layout state). A deserialization step produced nil where an empty string or 0 was intended. An MCP or script client serializes a struct with an unset pointer field.","solutions":["Replace nil with an explicit empty value: \"\" for strings, 0 for numbers, false for bools, or an empty object/array.","If the intent is to remove the key, call /api/storage/removeLocalStorageVals with that key instead.","Filter null entries out of the map on the client before sending."],"exampleFix":"// before\n{ \"keyVals\": { \"layout\": null } } // -> error 902\n// after\n{ \"keyVals\": { \"layout\": \"\" } }\n// or to delete: POST /api/storage/removeLocalStorageVals { \"keys\": [\"layout\"] }","handlingStrategy":"validation","validationCode":"// TypeScript: strip nulls, coerce to explicit empties, or route to remove\nfunction sanitizeKeyVals(kv: Record<string, unknown>) {\n  const out: Record<string, unknown> = {};\n  for (const [k, v] of Object.entries(kv)) {\n    if (v === null || v === undefined) continue; // or call removeLocalStorageVals\n    out[k] = v;\n  }\n  return out;\n}","typeGuard":"// TypeScript\nconst isStorableValue = (v: unknown): boolean => v !== null && v !== undefined;","tryCatchPattern":null,"preventionTips":["Never serialize undefined/null as a localStorage value; treat absence as a delete request.","Distinguish 'set to empty' (send \"\") from 'delete' (call remove API).","Validate the payload map on the client; reject keys with null before fetch."],"tags":["local-storage","validation","kernel","api"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}