Skip to content

useAgentChat: /get-messages fires with stale socket URL (and old auth token) when name prop changes #1864

Description

@ColinMcCullough

Summary

When the name prop passed to useAgent changes (e.g. on logout → new guest session), useAgentChat's initial messages fetch fires during the same render using the old socket's URL — which still contains the previous session's auth token. This causes two problems:

  1. Credential leak: a signed-in user's auth token is sent to a new (guest) endpoint after logout.
  2. Data poisoning: the old conversation's messages are cached under the new session's cache key. Because the key is already populated when the new socket's URL arrives, no corrective re-fetch occurs — the stale messages persist for the lifetime of the component.

Root cause

In packages/agents/src/chat/react.tsx, initialMessagesCacheKey is prop-derived (agentAddressKey, set synchronously from options.name every render). When name changes:

  1. agentAddressKey → new value (sync, this render)
  2. requestCache miss → doGetInitialMessages fires immediately, with agentUrlString = agent.getHttpUrl() = the old socket's _url (still carries old conversationId in path and old ?token=...)
  3. useStableSocket replaces the socket in a useEffectafter this render — so _url is still stale when the fetch fires

The fetch URL and the cache key are therefore from different renders, violating the invariant that was safe in @cloudflare/ai-chat@0.4.x (where the cache key was derived from agentUrl.pathname, which lagged with the socket).

Why the obvious fix (use resolvedInitialMessagesCacheKey) regresses #1356

resolvedInitialMessagesCacheKey (line 768) is the URL-inclusive key that would fix this, but it was deliberately excluded from doGetInitialMessages because including it causes a Suspense re-trigger on the null → resolved URL transition at first mount. The comment at lines 755–763 describes this exactly.

Reproduction

  1. Mount useAgentChat with useAgent({ name: convA, query: { token: tokenA } })
  2. Change props to { name: convB, query: { token: tokenB } }
  3. Observe: /get-messages fires with the URL for convA?token=tokenA
  4. The returned messages (from convA) are cached under convB's key
  5. When the new socket arrives with convB's URL, no re-fetch occurs (cache hit on the poisoned entry)
  6. Chat displays convA's messages under convB's session

What would fix it

The two cases that need different behavior:

Transition Desired behavior
agentUrl null → resolved (first mount handshake) Do NOT bust cache — existing behavior, avoids #1356
name changes while agentUrl is already non-null (session switch) DO use URL-inclusive key — wait for new socket URL before fetching

A fix that distinguishes these two cases (e.g. tracking whether the previous agentUrl.pathname was non-null before the name change) would restore the 0.4.x invariant without reintroducing #1356.

Workaround

Key the component mounting useAgentChat on name (force remount on session switch). This avoids the stale-fetch render entirely since the new mount's agent.getHttpUrl() is already correct.

Versions

  • agents: 0.17.3 (latest)
  • @cloudflare/ai-chat: 0.9.3 (latest, now a re-export of agents/chat/react)
  • Regression introduced when useAgentChat moved from @cloudflare/ai-chat/dist/react.js (0.4.x, inline) into agents/src/chat/react.tsx

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions