fix(ai-chat): don't crash when useAgent()'s HTTP URL isn't ready yet#1358
fix(ai-chat): don't crash when useAgent()'s HTTP URL isn't ready yet#1358threepointone merged 3 commits intomainfrom
Conversation
`useAgentChat()` called `new URL(agent.getHttpUrl())` unconditionally, which threw on first render whenever `useAgent()` hadn't populated its WebSocket URL yet — most commonly behind a proxy or with custom-routed workers. See #1356. Guards the URL parse, defers the built-in `/get-messages` fetch until the socket URL is known, keeps custom `getInitialMessages` loaders working with `url?: string`, stabilizes the underlying `useChat` `id` across the URL-arrival transition, and seeds messages exactly once when the URL becomes available — without clobbering user-driven clears. Made-with: Cursor
🦋 Changeset detectedLatest commit: 37e0093 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
…transition The request-dedup cache was keyed by origin+pathname+identity, so when the WebSocket URL transitioned from empty to resolved on the second render, `doGetInitialMessages` missed the cache, called the custom loader a second time, and `use(newPromise)` re-triggered Suspense — the user saw a loading fallback flash even though messages were already displayed. Cache by agent identity only. The URL-aware key survives as `resolvedInitialMessagesCacheKey` for the `stableChatIdRef` logic, which still needs to distinguish "URL arrived" from "identity changed." Regression test locked in: `should invoke custom getInitialMessages only once across the HTTP URL transition`. Made-with: Cursor
|
Good catch — confirmed the reproduction: with a custom Fixed in 5f6e794 by caching initial messages by agent identity only. The URL-aware key still exists as Added a regression test — |
…ady has the messages When the HTTP URL was available on the very first render, `useChat` seeded its Chat with `initialMessages` directly, so `chatMessages.length > 0` by the time the late-seed effect first ran. The effect short-circuited without calling `markInitialMessagesSeeded()`, leaving the ref at `null` forever — which meant any subsequent path that emptied the chat without going through the wrapper (most notably a server-originated `CF_AGENT_CHAT_MESSAGES` broadcast with `[]`, e.g. another tab calling `setMessages([])`) would trip all three guards and re-hydrate the stale initial messages on top of the clear. Mark the key seeded on every observation where the chat is in a settled state for this identity, not just when we actively inject messages. Regression test: `should not re-hydrate initial messages when a server broadcast empties the chat`. Made-with: Cursor
|
Valid too — confirmed the re-hydration race with a failing test before the fix. Reproduction: Fixed in 37e0093 by marking the cache key seeded on every settled observation, not just when we actively inject messages. The chat-is-already-populated branch now marks and returns. Regression test: |
|
Good observation, and the conclusion is correct. Two structural guarantees back it up beyond "works in practice":
So the ordering dependency isn't a timing race — it's enforced by React. One edge case that isn't protected, not flagged in the comment but worth being honest about: two Resolving that would require lifting the "seeded" record to module-level state keyed by identity. Given how niche the co-mounted-duplicate-hook pattern is, and that it interacts with the already-module-level |
Summary
Closes #1356.
useAgentChat()callednew URL(agent.getHttpUrl())unconditionally during render.useAgent()buildsgetHttpUrl()from PartySocket internals that can legitimately still be""before the WebSocket connects, which happens most often when the agent is behind a proxy or reached viabasePath/custom routing. That threwTypeError: Failed to construct 'URL': Invalid URLand crashed the component on first render, well before the handshake completed.This PR treats "HTTP URL not ready yet" as a first-class state in
useAgentChat():new URL(...)so an empty result just yieldsnull./get-messagesfetch until the URL is available, and the default fetcher returns[]if called with an empty URL.getInitialMessagescallbacks runnable before the URL exists.GetInitialMessagesOptions.urlis nowstring | undefined; callers that previously typedurl: stringshould widen tourl?: string.useChatidacross the URL-arrival transition so the AI SDK doesn't recreate the underlyingChat(and abandon in-flight resume) just because the URL materialized on render 2.clearHistory(),CF_AGENT_CHAT_CLEAR, and explicitsetMessages([])so user-driven clears aren't re-hydrated.No API break. Apps where
getHttpUrl()was synchronously available on first render are unchanged.Test plan
"should wait for a valid HTTP URL before fetching initial messages"— starts with an empty URL, verifies nofetch()happens, then makes the URL available and confirms/get-messagesis called exactly once with the normalized HTTP URL."should allow custom initial message loaders before the HTTP URL is ready"— verifies a user-providedgetInitialMessagesruns withurl: undefinedand seeds the chat.npm run test:reactinpackages/ai-chat— 47/47 passing.npm run checkat the repo root — formatting, lints, exports, and all 75 typecheck projects pass.Changeset
A patch-level changeset for
@cloudflare/ai-chatis included at.changeset/ai-chat-pending-http-url.md.Made with Cursor