Skip to content

Browser store build can crash with a proxy invariant error when a getter returns a leaked produce draft proxy #2668

@Hona

Description

@Hona

Describe the bug

In the browser store build, createStore + produce can crash with a JS proxy invariant error when a getter returns a leaked produce draft proxy.

This reproduces in isolation with public APIs only. No raw state mutation, unwrap, or internal symbol access is needed.

Actual error:

TypeError: 'get' on proxy: property 'Symbol(solid-proxy)' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value

This does not reproduce with Node's default solid-js/store resolution, because that loads the server build. It does reproduce with the browser build.

Your Example Website or App

opencode e2e tests

Steps to Reproduce the Bug or Issue

  1. Create repro.mjs with:
import { createStore, produce } from "solid-js/store"

let leaked
const [store, setStore] = createStore({
  items: [],
  get probe() {
    return (this.items, leaked)
  },
})

setStore(produce((draft) => {
  leaked = draft.items
  store.probe
}))
  1. Run:
node --conditions=browser repro.mjs
  1. Observe the proxy invariant error above.

If it helps, the same repro also works with plain node repro.mjs if the import is changed to:

import { createStore, produce } from "solid-js/store/dist/store.js"

Expected behavior

I would expect this to not hard-crash inside Solid.

Even if this is considered a sharp/unsupported edge case, I would expect one of these instead:

  • the value is sanitized before it re-enters store machinery
  • the getter result is safely ignored/rejected
  • Solid throws a normal framework error instead of violating a JS proxy invariant

Screenshots or Videos

None.

Platform

  • OS: Windows 11
  • Browser: N/A; reproducing the browser store build via node --conditions=browser
  • Version: Node 24.14.1
  • solid-js: 1.9.10

Additional context

This appears to be specific to the browser store implementation.

From reading the built source, the likely path is:

  • wrap / store proxying attaches Symbol(solid-proxy) to the raw array
  • produce creates a draft proxy for that same raw array
  • unwrap() skips getter properties, so a getter can return the leaked draft proxy through a normal setStore(...) path
  • a later read hits setterTraps.get -> isWrappable(value), which reads value[$PROXY]
  • that violates the proxy invariant because the target already has a non-configurable Symbol(solid-proxy) property with a different value

Relevant locations in store/dist/store.js from solid-js@1.9.10:

  • isWrappable: line 31
  • browser store proxy get: line 103
  • unwrap: getter skip at line 49
  • setterTraps.get: line 399
  • produce: line 414

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions