How Read Frog Handles Stream Edge Cases and Error Recovery Mechanisms for AI Responses
Read Frog ensures robust AI streaming by validating payloads with Zod schemas, enforcing single terminal states via a settled flag, capturing AI-SDK errors through onError callbacks, and maintaining connection liveness with keep-alive pings.
Read Frog is a Chrome extension that streams AI-generated content between content scripts and background workers using Chrome's runtime.Port API. Handling stream edge cases and error recovery mechanisms is critical for maintaining user experience when network interruptions, malformed payloads, or AI service failures occur.
Background-Side Stream Orchestration and Validation
The core stream handling logic resides in src/entrypoints/background/background-stream.ts. This module orchestrates the connection between the AI SDK and the Chrome extension port, implementing multiple defensive layers against edge cases.
Payload Validation with Zod Schemas
Every stream begins with a start message that must conform to strict schemas. The createStartMessageParser function validates incoming payloads using either streamTextPayloadSchema or structuredObjectPayloadSchema depending on the stream type.
If Zod parsing fails, the handler immediately sends an error message with the text "Invalid stream start payload", disconnects the port, and prevents any further processing. This validation occurs before the AI stream is initiated, ensuring that malformed requests never reach the AI provider.
Single-Completion Guards and Terminal State Enforcement
To prevent duplicate terminal messages or resource leaks, createStreamPortHandler maintains three state flags: isActive, hasStarted, and settled. The settled flag acts as a single-completion guard.
The finalize function checks this flag before performing cleanup:
const finalize = (callback: () => void) => {
if (settled) return
settled = true
try { port.disconnect() } catch {}
callback()
cleanup()
}
This pattern ensures that exactly one terminal message (done or error) is sent and that the port disconnects only once, even if multiple error conditions trigger simultaneously.
Abort Signal Handling and Cleanup
Each stream creates a dedicated AbortController. If the caller provides an AbortSignal that is already aborted before the stream starts, the handler immediately throws a DOMException with the name "AbortError" and message "stream aborted".
During active streaming, the abort signal is passed to the AI SDK's streamFn. When the signal fires, the onError callback captures the abort as an error, triggering the error recovery path and ensuring the port posts an error message before disconnecting.
The disconnectListener also aborts the controller when the port disconnects unexpectedly, preventing orphaned streams from continuing to consume resources after the connection is lost.
Client-Side Port Streaming and Connection Management
The content script uses src/utils/content-script/port-streaming.ts to wrap the Chrome port in a Promise-based API that handles message correlation, keep-alive mechanics, and abort integration.
Request-ID Correlation and Message Filtering
To prevent message leakage between concurrent streams, each call to createPortStreamPromise generates a UUID using createRequestId. This requestId is attached to every message sent to the background script.
The message listener filters incoming port messages by requestId, ignoring any stray messages from previous or concurrent streams that might arrive due to timing issues or port reuse. This correlation ensures that a stream only resolves or rejects based on its own terminal message.
Keep-Alive Ping Mechanism
Chrome ports can disconnect due to inactivity during long-running AI generations. To prevent this, the client implements a keep-alive ping system:
if (keepAliveIntervalMs > 0) {
keepAliveTimer = setInterval(() => {
if (settled) return
try {
const pingMessage: StreamPortRequestMessage<unknown> = { type: "ping", requestId }
port.postMessage(pingMessage)
} catch {}
}, keepAliveIntervalMs)
}
By default, keepAliveIntervalMs is set to 20 seconds. The background handler explicitly ignores ping messages until the stream has started, preventing premature activation while still maintaining the connection during active streaming.
Abort Integration and Error Propagation
If the caller provides an AbortSignal, the promise immediately rejects with a DOMException("aborted", "AbortError") if the signal is already aborted. Otherwise, the abort listener attaches to the signal and will reject the promise when triggered.
When the background posts an error message, the client wraps the error payload in a JavaScript Error instance and rejects the promise, propagating the error message to the caller. This ensures that AI-SDK errors, network failures, or validation errors all surface as standard promise rejections in the content script.
AI Error Recovery and Message Normalization
To present meaningful error messages to users, Read Frog implements specialized error extraction logic in src/utils/error/extract-message.ts.
The extractAISDKErrorMessage function handles the various shapes of errors thrown by the AI SDK:
export function extractAISDKErrorMessage(error: unknown): string {
if (typeof error === "string") return error
if (typeof error === "object" && error !== null) {
const src = error as { message?: unknown; responseBody?: unknown; text?: unknown }
return getNonEmptyString(src.message) ??
getNonEmptyString(src.responseBody) ??
getNonEmptyString(src.text) ??
"Unexpected error occurred"
}
return "Unexpected error occurred"
}
This extraction prioritizes the message property, falls back to responseBody or text for raw API responses, and ensures that users always see a human-readable string rather than [object Object] or undefined values. When the background handler captures an error via the onError callback, it uses this utility to normalize the message before posting it to the content script.
End-to-End Implementation Example
The following example demonstrates how these mechanisms work together in practice:
// content-script: request a streamed translation
import { createPortStreamPromise } from "@/utils/content-script/port-streaming"
import { BACKGROUND_STREAM_PORTS } from "@/types/background-stream"
async function translateSelection(text: string, abortSignal?: AbortSignal) {
const payload = { providerId: "openai-default", prompt: `Translate: ${text}` }
// Returns the complete translated string when the background finishes
const translated = await createPortStreamPromise<string>(
BACKGROUND_STREAM_PORTS.streamText,
payload,
{ signal: abortSignal, onChunk: chunk => console.log("Chunk:", chunk) }
)
return translated
}
Behind the scenes:
createPortStreamPromisegenerates a UUIDrequestIdand sends astartmessage to the background port.- The background handler (
handleStreamTextPortinsrc/entrypoints/background/background-stream.ts) validates the payload using Zod schemas. If invalid, it immediately posts anerrorwith "Invalid stream start payload". - Upon validation, the handler creates an
AbortControllerand callsrunStreamTextInBackground, passing the abort signal to the AI SDK. - Chunks stream via
port.postMessagewithtype: "chunk". If the AI SDK reports an error viaonError, the handler stores it instreamErrorand surfaces it usingextractAISDKErrorMessage. - The client receives messages, filtering by
requestId. Keep-alive pings fire every 20 seconds to prevent Chrome from disconnecting the port. - On
done, the promise resolves. Onerroror disconnect, it rejects with the normalized error message. Thefinalizefunction ensures the port disconnects exactly once.
Summary
- Strict validation using Zod schemas in
createStartMessageParserprevents malformed payloads from reaching the AI provider, returning "Invalid stream start payload" immediately on failure. - Single-completion guarantees via the
settledflag andfinalizefunction ensure exactly one terminal message (doneorerror) is sent and resources are cleaned up once. - Abort handling integrates
AbortControllerwith Chrome ports, throwingDOMException("stream aborted", "AbortError")when signals fire or ports disconnect unexpectedly. - Error prioritization captures the original AI-SDK error via
onErrorcallbacks, usingextractAISDKErrorMessageto normalize various error shapes into user-friendly strings. - Connection resilience uses UUID-based
requestIdcorrelation to filter stray messages and keep-alive pings (default 20s) to prevent Chrome from garbage-collecting idle ports.
Frequently Asked Questions
How does Read Frog prevent duplicate terminal messages?
Read Frog uses a settled flag within the finalize function in src/entrypoints/background/background-stream.ts. When a stream completes—whether successfully with done or unsuccessfully with error—the finalize function checks this flag. If already true, it returns immediately without sending additional messages or disconnecting the port again. This guarantees exactly one terminal event per stream even if multiple error conditions trigger simultaneously.
What happens when an AI stream aborts mid-generation?
When an abort signal fires or the port disconnects unexpectedly, the AbortController created in createStreamPortHandler triggers cancellation. The background handler catches this via the onError callback, stores the error in streamError, and posts an error message to the content script before calling finalize. The content script's createPortStreamPromise receives this error and rejects the promise with a JavaScript Error instance, allowing the UI to display the failure reason extracted via extractAISDKErrorMessage.
How are AI-SDK error messages extracted for display?
The extractAISDKErrorMessage function in src/utils/error/extract-message.ts normalizes errors by checking multiple possible shapes. It first checks if the error is a string, then inspects object properties for message, responseBody, or text fields. It returns the first non-empty string found, or falls back to "Unexpected error occurred". This ensures users see meaningful descriptions rather than [object Object] or undefined values, regardless of how the AI SDK structures its errors.
Why does Read Frog use keep-alive pings for Port connections?
Chrome's runtime.Port connections can be garbage-collected or disconnected by the browser if no messages flow for extended periods, which commonly occurs during long-running AI generations. To prevent silent disconnects, createPortStreamPromise in src/utils/content-script/port-streaming.ts sends ping messages every 20 seconds (configurable via keepAliveIntervalMs) while the stream is active. The background handler ignores these pings until the stream starts, ensuring the connection stays alive without interfering with the actual data flow.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →