Skip to content

fix(ai-chat): terminate WebSocket chat transport stream on abort#986

Merged
threepointone merged 1 commit intomainfrom
fix/ws-chat-transport-abort-stream
Feb 25, 2026
Merged

fix(ai-chat): terminate WebSocket chat transport stream on abort#986
threepointone merged 1 commit intomainfrom
fix/ws-chat-transport-abort-stream

Conversation

@threepointone
Copy link
Contributor

Summary

Fixes #985

The ReadableStream returned by WebSocketChatTransport.sendMessages() was never terminated when the caller's AbortSignal fired or stream.cancel() was called. This left useChat stuck in the "streaming" state after the user clicked Stop/Cancel.

What changed

packages/ai-chat/src/ws-chat-transport.ts

  • All three terminal paths (done, error, abort) now funnel through a single finish(action) helper that gates on a completed flag, cleans up activeRequestIds, and detaches the WS listener via abortController.abort().
  • onAbort() sends CF_AGENT_CHAT_REQUEST_CANCEL to the server (best-effort), then errors the stream with an AbortError via finish().
  • stream.cancel() delegates to onAbort(), so cancelling the stream from the consumer side also notifies the server.
  • The caller's abortSignal listener is registered with { once: true } and handles already-aborted signals.

packages/ai-chat/src/tests/ws-transport-abort.test.ts (new)

Four test cases exercising the transport directly:

  1. Mid-stream abort — aborts after 200ms, verifies the stream terminates (the original regression)
  2. Pre-aborted signal — passes an already-aborted signal, verifies immediate termination
  3. stream.cancel() — verifies cancel completes without hanging
  4. Normal completion — sanity check that the happy path still works through the transport

How this differs from the proposed fix in #985

The community fix is correct and solves the bug, but our implementation simplifies the approach:

Aspect Proposed fix This PR
State flags Two flags (abortRequested + completed) One flag (completed)
Cleanup helpers Two functions (requestAbort + completeAbort) One function (finish)
start() race guard Checks abortRequested in start() to handle "abort before stream starts" Removed — start() runs synchronously during ReadableStream construction, so streamController is always set before any listener can fire
Cleanup duplication done/error paths in onMessage have separate inline cleanup All paths use finish()
Tests 1 test (mid-stream abort) 4 tests

Notes for reviewers

  • Definite assignment assertion: let streamController!: ReadableStreamDefaultController uses ! which is new to this codebase. The comment above it explains why it is safe (synchronous start()). If you prefer | undefined with optional chaining, happy to change.
  • Pre-aborted signal still sends the WS request: If the signal is already aborted when sendMessages() is called, onAbort() fires immediately but the request is still sent on the next line. This is harmless — the server receives the cancel and stops, and the client stream is already errored. Adding a guard would be possible but adds complexity for a negligible edge case.
  • Hibernation: This change is client-side only and does not affect the server-side resumable streaming mechanism. Stream resumption after hibernation/reconnect flows through the useAgentChat hook's onAgentMessage handler (CF_AGENT_STREAM_RESUMING protocol), which is a separate path from the transport's ReadableStream.
  • All 206 tests pass across 25 test files (npx vitest --project workers --run).

Close the ReadableStream returned by WebSocketChatTransport.sendMessages()
when the caller's AbortSignal fires or stream.cancel() is called. Without
this, useChat remains stuck in the 'streaming' state after stop/cancel.

- Consolidate done/error/abort cleanup into a single finish() helper
- Send CF_AGENT_CHAT_REQUEST_CANCEL on abort and stream.cancel()
- Add tests for mid-stream abort, pre-aborted signal, stream.cancel(),
  and normal completion through the transport

Fixes #985
@changeset-bot
Copy link

changeset-bot bot commented Feb 25, 2026

🦋 Changeset detected

Latest commit: a3c2897

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/ai-chat Patch

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 25, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@986
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/ai-chat@986
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/codemode@986
npm i https://pkg.pr.new/cloudflare/agents/hono-agents@986

commit: a3c2897

@threepointone threepointone merged commit e0d7a75 into main Feb 25, 2026
4 checks passed
@threepointone threepointone deleted the fix/ws-chat-transport-abort-stream branch February 25, 2026 12:49
@github-actions github-actions bot mentioned this pull request Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(ai-chat): terminate WebSocketChatTransport stream on abort

1 participant