Skip to content

WorkflowChatTransport.reconnectToStreamIterator logs console.error on intentional client abort #1971

@shtefcs

Description

@shtefcs

WorkflowChatTransport.reconnectToStreamIterator catches every error from the chunk stream and calls console.error('Error in chat GET reconnectToStream', error) unconditionally, including when the error is an AbortError from an intentional client abort (Stop button, tab close, page navigation). The abort is otherwise handled correctly (status transitions back to ready, no leaks), but the browser console fills with what look like real errors on every successful stop.

The same pattern exists for the POST path: console.error('Error in chat POST stream', error).

Versions

  • workflow@5.0.0-beta.4-30cc3a9
  • @workflow/ai@5.0.0-beta.3-30cc3a9
  • ai@6.0.141
  • Next.js 16, React 19

Repro

App uses a custom transport over WorkflowChatTransport with a fetch wrapper that combines the user's stop AbortController with the per-request abort signal. When the user clicks Stop or closes the tab while a stream is open, the abort propagates correctly and useChat transitions out of streaming, but the console shows:

Error in chat GET reconnectToStream AbortError: BodyStreamBuffer was aborted
    at b67ef0a7d90ce1c7.js:1:28872
    at t (5c038c7ff70e563c.js:279:13938)
    at sX (1a7dc033a655b4da.js:1:155155)
    ...
    at pull
    at setTimeout

Up to maxConsecutiveErrors repetitions per stop (default 3) before the iterator gives up and exits.

Location

packages/ai/src/workflow-chat-transport.ts, inside reconnectToStreamIterator, the catch block around the for await (const chunk of streamToIterator(chunkStream)) loop. In the published dist this is node_modules/@workflow/ai/dist/workflow-chat-transport.js:242:

catch (error) {
  console.error('Error in chat GET reconnectToStream', error);
  consecutiveErrors++;
  if (consecutiveErrors >= this.maxConsecutiveErrors) {
    throw new Error(
      `Failed to reconnect after ${this.maxConsecutiveErrors} consecutive errors. Last error: ${getErrorMessage(error)}`
    );
  }
}

Same shape on the POST path around line 129.

Canonical pattern in vercel/ai

The base AI SDK does not log inside its transport. HttpChatTransport.reconnectToStream simply throws on error and delegates error policy to the consumer. The consumer is Chat in packages/ai/src/ui/chat.ts, which has the established AbortError handling at lines 651-666 and 734-740:

activeResponse.abortController.signal.addEventListener('abort', () => {
  isAbort = true;
});
...
catch (err) {
  // Ignore abort errors as they are expected.
  if (isAbort || (err as any).name === 'AbortError') {
    isAbort = true;
    this.setStatus({ status: 'ready' });
    return null;
  }
  // real error handling
}

WorkflowChatTransport breaks this convention by swallowing the error inside its own iterator and emitting console.error for it, so the Chat class never gets a chance to special-case the abort.

Suggested fix

Either of these works:

1. Guard the existing log.

catch (error) {
  const isAbort =
    (error as any)?.name === 'AbortError' ||
    options.abortSignal?.aborted === true;
  if (!isAbort) {
    console.error('Error in chat GET reconnectToStream', error);
    consecutiveErrors++;
    if (consecutiveErrors >= this.maxConsecutiveErrors) {
      throw new Error(...);
    }
  } else {
    // Intentional abort: exit the loop cleanly.
    return;
  }
}

2. Mirror vercel/ai's pattern more directly. Detect abort and return without logging or counting toward consecutiveErrors. This also avoids the misleading Failed to reconnect after N consecutive errors rethrow when the only errors were aborts.

Same treatment is needed for Error in chat POST stream on the send path.

Why this matters

For apps that wire a real Stop button, or rely on tab close to terminate the stream, this log fires on every successful stop. It looks like a regression to the user, creates noise during real debugging, and a strict CI that asserts no console.error will fail.

Related issues and PRs

Happy to send a PR if useful. Otherwise flagging here in case #1847 can pick this up while that area is being rewritten.

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