cloudflare / cloudflare/agents

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

Open
#1,864 0 comments 0 reactions 1 assignee Claimed by @threepointone View on GitHub
bug
Dominant language
TypeScript
Stars
5.6k
Forks
711
Avg merge
1d 20h
Merged PRs (30d)
53

Description

## 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 `useEffect` — **after** 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`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.