From a94fe680a5bc2f5b65d306511d8aa79d4a3eeb08 Mon Sep 17 00:00:00 2001 From: Kim Biesbjerg Date: Sun, 15 Feb 2026 16:26:11 +0100 Subject: [PATCH] fix: safely serialize query/mutation data to prevent DataCloneError with framework proxies Frameworks like Vue 3, MobX, and Solid wrap state in Proxy objects. When mapQueryToData / mapMutationToData pass these values directly into the payload sent via window.postMessage, the structured clone algorithm fails with a DataCloneError. Add a safeClone helper that JSON-round-trips values before they are included in the message payload. This reads through Proxy getters and produces plain objects that are safe for structured cloning. Co-authored-by: Cursor --- src/injected/injected.ts | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/injected/injected.ts b/src/injected/injected.ts index cadf941..6628fdb 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -19,6 +19,20 @@ import { import { TanStackQueryActionExecutor } from "./modules/action-executor"; import { sendToContentScript } from "./modules/message-sender"; +/** + * Safely deep-clone a value into a plain, structured-cloneable object. + * Frameworks like Vue 3, MobX, and Solid wrap state in Proxy objects that + * cannot be passed through `window.postMessage` (structured clone algorithm). + * JSON round-tripping reads through Proxy getters, producing plain objects. + */ +function safeClone(value: T): T { + try { + return JSON.parse(JSON.stringify(value)); + } catch { + return "[Unserializable]" as T; + } +} + // Main injected script class class InjectedScript { private detectionCleanup: (() => void) | null = null; @@ -282,11 +296,11 @@ class InjectedScript { // Map Query object to QueryData format private mapQueryToData(query: Query): QueryData { return { - queryKey: query.queryKey, + queryKey: safeClone(query.queryKey), queryHash: query.queryHash, state: { - data: query.state.data, - error: query.state.error, + data: safeClone(query.state.data), + error: safeClone(query.state.error), status: query.state.status, isFetching: query.state.fetchStatus === "fetching", isPending: query.state.status === "pending", @@ -298,7 +312,7 @@ class InjectedScript { errorUpdatedAt: query.state.errorUpdatedAt, fetchStatus: query.state.fetchStatus, }, - meta: query.meta || {}, + meta: safeClone(query.meta || {}), isActive: query.getObserversCount() > 0, observersCount: query.getObserversCount(), }; @@ -309,10 +323,10 @@ class InjectedScript { return { mutationId: mutation.mutationId, state: mutation.state.status, - variables: mutation.state.variables, - context: mutation.state.context, - data: mutation.state.data, - error: mutation.state.error, + variables: safeClone(mutation.state.variables), + context: safeClone(mutation.state.context), + data: safeClone(mutation.state.data), + error: safeClone(mutation.state.error), submittedAt: mutation.state.submittedAt, isPending: mutation.state.status === "pending", };