siyuan-note/siyuan · error

path %v: expected object, got %T

Error message

path %v: expected object, got %T

What it means

Thrown by getJsContextValue when cursor.ToObject(rt) returns nil — the cursor value is neither nil nor goja-undefined/null but cannot be converted to a goja.Object (e.g. a primitive string/number/boolean). The %T reports the underlying Go type. Means code tried to index into a non-object value.

Source

Thrown at kernel/plugin/sandbox.go:304

// getJsContextValue safely retrieves a nested value from the plugin's JS context, returning nil if any step fails.
func getJsContextValue(rt *goja.Runtime, paths []any) (value goja.Value, err error) {
	var cursor goja.Value = rt.GlobalObject()
	var path string = "globalThis"

	for _, key := range paths {
		if cursor == nil {
			err = fmt.Errorf("path %v: value is nil", key)
			return
		}

		if goja.IsUndefined(cursor) || goja.IsNull(cursor) {
			err = fmt.Errorf("path %v: value is %s", key, cursor.String())
			return
		}

		obj := cursor.ToObject(rt)
		if obj == nil {
			err = fmt.Errorf("path %v: expected object, got %T", key, cursor)
			return
		}

		switch k := key.(type) {
		case string:
			cursor = obj.Get(k)
			path = fmt.Sprintf("%s.%s", path, k)
		case int:
			cursor = obj.Get(strconv.Itoa(k))
			path = fmt.Sprintf("%s[%d]", path, k)
		default:
			err = fmt.Errorf("unsupported path type: %T", key)
			return
		}
	}
	value = cursor
	return
}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Audit globalThis.siyuan assignments in the plugin: every node the kernel traverses (siyuan, siyuan.server, scope, requestType) must be a plain object.
  2. Add a typeof === 'object' && !Array.isArray guard in plugin code before assigning each level.
  3. If the error names a primitive type (string/int/bool), grep the plugin source for direct reassignment of that path.

Example fix

// before
globalThis.siyuan = JSON.stringify({server:{}}) // oops: string
// after
globalThis.siyuan = { server: {} }
Defensive patterns

Strategy: type-guard

Validate before calling

// Reject non-object intermediate before the kernel traverses it.
function assertObjectAt(path: string[]): void {
  let cur: any = globalThis
  for (const k of path) {
    cur = cur[k]
    if (typeof cur !== 'object' || cur === null || Array.isArray(cur)) {
      throw new Error(`path ${path.join('.')}: expected object at ${k}`)
    }
  }
}

Type guard

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v)
}

Prevention

When it happens

Trigger: globalThis.siyuan was set to a string/number instead of an object; siyuan.server was set to a function or primitive; an intermediate path was reassigned from object to scalar by buggy plugin code.

Common situations: Plugin author writes globalThis.siyuan = "..."; a hook overwrote an object path with a scalar return value; serialization round-trip flattened an object to a string.

Related errors


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