siyuan-note/siyuan · error

local storage value for key [%s] must not be empty

Error message

local storage value for key [%s] must not be empty

What it means

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.

Source

Thrown at kernel/model/storage.go:61

	defer localStorageLock.Unlock()
	return getLocalStorage()
}

func SetLocalStorage(val map[string]any) (err error) {
	localStorageLock.Lock()
	defer localStorageLock.Unlock()
	return setLocalStorage(val)
}

func SetLocalStorageVals(keyVals map[string]any) (setKeyVals map[string]any, err error) {
	localStorageLock.Lock()
	defer localStorageLock.Unlock()

	setKeyVals = make(map[string]any, len(keyVals))
	localStorage := getLocalStorage()
	for k, v := range keyVals {
		if v == nil {
			err = fmt.Errorf("local storage value for key [%s] must not be empty", k)
			return
		}
		localStorage[k] = v
		setKeyVals[k] = v
	}
	err = setLocalStorage(localStorage)
	return
}

func RemoveLocalStorageVals(keys []string) (err error) {
	localStorageLock.Lock()
	defer localStorageLock.Unlock()

	localStorage := getLocalStorage()
	for _, key := range keys {
		delete(localStorage, key)
	}
	return setLocalStorage(localStorage)

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Replace nil with an explicit empty value: "" for strings, 0 for numbers, false for bools, or an empty object/array.
  2. If the intent is to remove the key, call /api/storage/removeLocalStorageVals with that key instead.
  3. Filter null entries out of the map on the client before sending.

Example fix

// before
{ "keyVals": { "layout": null } } // -> error 902
// after
{ "keyVals": { "layout": "" } }
// or to delete: POST /api/storage/removeLocalStorageVals { "keys": ["layout"] }
Defensive patterns

Strategy: validation

Validate before calling

// TypeScript: strip nulls, coerce to explicit empties, or route to remove
function sanitizeKeyVals(kv: Record<string, unknown>) {
  const out: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(kv)) {
    if (v === null || v === undefined) continue; // or call removeLocalStorageVals
    out[k] = v;
  }
  return out;
}

Type guard

// TypeScript
const isStorableValue = (v: unknown): boolean => v !== null && v !== undefined;

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/84a1a0b65752fdbe. Report an issue: GitHub.