Skip to content

[query-broadcast-client-experimental] postMessage failures surface as unhandled DataCloneError rejections #10542

@SutuSebastian

Description

@SutuSebastian

Describe the bug

broadcastQueryClient calls channel.postMessage(...) three times (on updated, removed, added) without .catch()-ing the returned promise — see src/index.ts#L39-L62.

When a query's state.data (or state.error, or queryKey) contains a value the structured-clone algorithm can't serialize — ReadableStream, Response, Request, File, functions, framework proxies (Vue reactive, MobX, etc.) — broadcast-channel rejects internally and the rejection becomes an unhandledrejection:

DataCloneError: Failed to execute 'postMessage' on 'BroadcastChannel':
  A ReadableStream could not be cloned because it was not transferred.
    at postMessage (broadcast-channel/.../methods/native.js:24)

The stack points into node_modules with no indication of which query is at fault, so it's effectively unactionable when it shows up in Sentry / Datadog. Same root cause as #8356 (Vue reactive proxy in Nuxt) which was closed via a user-side toRaw() workaround — but that fix doesn't generalize: any query whose data happens to contain a non-cloneable value silently kills cross-tab sync for that update and spams error trackers.

Your minimal, reproducible example

Bug location: https://github.com/TanStack/query/blob/main/packages/query-broadcast-client-experimental/src/index.ts#L39-L62

Inline repro (5 lines of vanilla TS, no framework — happy to wire up a Stackblitz if needed but I think it'd just add ceremony around this snippet):

import { QueryClient } from '@tanstack/query-core'
import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental'

const queryClient = new QueryClient()
broadcastQueryClient({ queryClient, broadcastChannel: 'demo' })

window.addEventListener('unhandledrejection', (e) =>
  console.error('caught unhandled:', e.reason),
)

// any non-cloneable value triggers it. ReadableStream is the most common in
// the wild (Response.body, fetch streaming, AI SDK, etc.):
queryClient.setQueryData(['stream'], new Response('hi').body)

Steps to reproduce

  1. Set up broadcastQueryClient against any QueryClient.
  2. Cache any value containing a ReadableStream / Response / File / function / proxy.
  3. Observe an unhandled DataCloneError rejection in the console.

Expected behavior

broadcastQueryClient should not surface clone failures as unhandled rejections. Cross-tab sync of one query should not crash silently, nor should it pollute consumers' error trackers with stacks that point into node_modules and don't identify the offending queryHash.

How often does this bug happen?

Every time

Platform

  • OS: any (Chrome on macOS in our case)
  • Browser: any with BroadcastChannel support
  • Version: n/a

Tanstack Query adapter

vanilla (the broadcast client itself)

TanStack Query version

@tanstack/query-broadcast-client-experimental@5.99.0 (current latest), @tanstack/query-core@5.x

TypeScript version

5.x

Additional context

Two possible fixes — happy to PR either flavor:

1. Minimal — just `.catch()` and warn in dev:

```ts
const safePost = (message: BroadcastMessage) => {
channel.postMessage(message).catch((error: unknown) => {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[broadcastQueryClient] failed to broadcast "${message.type}" for queryHash "${message.queryHash}":`,
error,
)
}
})
}
```

…then replace the three `channel.postMessage({...})` call sites with `safePost({...})`.

2. Hookable — same as (1) plus an opt-in `onBroadcastError` so consumers can pipe to Sentry/Datadog/etc. without forking:

```ts
interface BroadcastQueryClientOptions {
queryClient: QueryClient
broadcastChannel?: string
options?: BroadcastChannelOptions
onBroadcastError?: (error: unknown, message: BroadcastMessage) => void
}
```

Tests: extend `src/tests/index.test.ts` with a case that calls `setQueryData` with a `ReadableStream` and asserts no `unhandledrejection` fires (and, for flavor 2, that `onBroadcastError` is invoked with the offending `queryHash`).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions