How to Handle SSE Streaming with Abort Signals in OmniRoute: A Complete Guide
Use createSSEStream with an AbortSignal to enable client-initiated cancellation, automatic idle timeouts, and proper HTTP error codes (499/504) for aborted Server-Sent Event streams in OmniRoute.
OmniRoute provides a robust pipeline for handling Server-Sent Events (SSE) streaming with built-in support for abort signals, idle timeouts, and provider-side failure handling. This article explains how the createSSEStream transform in open-sse/utils/stream.ts manages request lifecycle and cancellation, with practical code examples you can use today.
Core Components of SSE Abort Handling
The createSSEStream Transform
The heart of OmniRoute's SSE handling is the createSSEStream transform, located in [open-sse/utils/stream.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/utils/stream.ts#L6222-LL6350) (lines 6222–6350). This factory function returns a transform stream that:
- Wraps upstream fetch responses
- Monitors for idle timeouts
- Respects external
AbortSignalinstances - Handles provider error events
- Manages cleanup of pending requests
Client-Initiated Abort Patterns
Passing an AbortSignal to Chat Handlers
Handlers in [open-sse/handlers/chat.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/handlers/chat.ts) forward the caller's AbortSignal directly to the underlying fetch. Here's how to use it:
import { fetchChatCompletions } from "@/open-sse/handlers/chat.ts";
// 1️⃣ Create a request-level AbortSignal with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5_000);
// 2️⃣ Pass the signal into the handler
await fetchChatCompletions({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Explain abort handling." }],
signal: controller.signal, // <- forwarded to fetch
onComplete: (payload) => console.log("✅ done", payload),
onFailure: (err) => console.warn("❌ failure", err),
});
clearTimeout(timeout);
HTTP Status Codes for Aborted SSE Streams
OmniRoute distinguishes between two abort scenarios based on timing:
| Scenario | HTTP Status | Test Reference |
|---|---|---|
| Abort before any data received | 499 | tests/unit/t3-chat-web.test.ts:312 |
| Abort after partial response | 504 | tests/unit/tryBackedChat.test.ts:137 |
The 499 status indicates a clean client cancellation, while 504 signals that a gateway timeout occurred with partial data already streamed.
Server-Side Timeout Protection
Upstream Body Timeout with withBodyTimeout
The helper function withBodyTimeout (lines 88–101) races the fetch body promise against a configurable timeout:
import { withBodyTimeout } from "@/open-sse/utils/stream.ts";
const bodyPromise = fetch(url, { signal }).then(r => r.text());
// Timeout body read after 8 seconds
const safeBody = await withBodyTimeout(bodyPromise, 8_000);
throws new BodyTimeoutError(); // if timeout fires
Idle Stream Watchdog
When createSSEStream starts, it creates an interval timer (lines 148–155) that monitors STREAM_IDLE_TIMEOUT_MS (default 30s). If no chunk arrives in time:
- The stream aborts internally
- A warning is logged
- The optional
onFailurecallback receives a syntheticstream_idle_timeoutpayload (lines 151–182)
{
onFailure: (failure) => {
if (failure.code === "stream_idle_timeout") {
console.error("No data from provider for 30s");
}
}
}
Handling Provider-Side Failures
Rate Limits and Usage Limits
createSSEStream listens for response.failed events from providers. These are forwarded to onFailure, where you can decide whether to swallow the error or let the stream error out:
await readTransformed(
[ `data: ${JSON.stringify({
type: "response.failed",
response: { error: { code: "rate_limit_exceeded" } }
})}\n\n` ],
{
mode: "translate",
sourceFormat: "openai",
targetFormat: "openai_responses",
provider: "codex",
model: "gpt-5.5",
onFailure: (failure) => {
if (failure.code === "rate_limit_exceeded") {
console.warn("Provider throttled:", failure.message);
return true; // swallow error, don't emit generic error
}
return false; // let stream error out
},
}
);
Tests demonstrating this behavior:
- "translate mode aborts on Responses failure with rate limit error" — lines 1270–1278
- "passthrough aborts on Responses usage-limit failures and reports 429" — lines 2109–2145
Cleanup and Request Bookkeeping
Pending Request Tracking
The transform tracks pendingRequestClearedFromStream and clears the pending-request entry in the usage database when:
- The stream ends normally
- The stream aborts (client or idle timeout)
- An upstream error occurs
This prevents dangling usage rows in the database (lines 149–155 in createSSEStream).
Format-Specific SSE Behavior
Conditional [DONE] Terminator
Not all clients expect the data: [DONE] terminator. createSSEStream computes shouldEmitDoneTerminator (lines 698–699) based on target format:
| Target Format | Emits [DONE]? |
|---|---|
| OpenAI Chat | Yes |
| OpenAI Responses | No |
| Claude | No |
| Antigravity | Yes |
Metadata Comments
When OMNIROUTE_SSE_COMMENTS is enabled, emitFinalSseMetadata (lines 1036–1054) emits a final metadata comment:
: x-omniroute-request-id=req_abc123; x-omniroute-tokens-used=847
This is omitted for strict JSON-parsing clients.
Testing Abort Signal Handling
Unit Test: External Abort Before Any Chunk
From [tests/unit/tryBackedChat.test.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/tests/unit/tryBackedChat.test.ts):
it("returns 504 when external AbortSignal is already aborted", async () => {
const ac = new AbortController();
ac.abort(); // abort immediately
await assert.rejects(
chatCore({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "ping" }],
signal: ac.signal, // passed straight to executor
}),
/Aborted|504/ // handler translates to 504
);
});
Abort Mid-Stream Test
From [tests/unit/t3-chat-web.test.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/tests/unit/t3-chat-web.test.ts):
it("execute: AbortSignal abort → returns 499", async () => {
const controller = new AbortController();
const promise = executeChat({
signal: controller.signal,
// ...
});
controller.abort(); // abort after execution starts
const result = await promise;
expect(result.status).toBe(499); // client closed request
});
Key Files Reference
| File | Purpose |
|---|---|
[open-sse/utils/stream.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/utils/stream.ts) |
Core SSE transform, idle watchdog, abort handling, [DONE] logic |
[open-sse/utils/streamErrorFormat.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/utils/streamErrorFormat.ts) |
Normalizes provider error payloads |
[open-sse/handlers/chat.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/handlers/chat.ts) |
Bridges API routes to executor, forwards signals |
[tests/unit/stream-utils.test.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/tests/unit/stream-utils.test.ts) |
Idle timeout, rate-limit, usage-limit, terminator tests |
Summary
- Use
AbortControllerto enable client-initiated cancellation in OmniRoute SSE streams - Expect HTTP 499 for aborts before data arrives, 504 for aborts after partial response
- Configure
withBodyTimeoutto protect against slow upstream body reads - Rely on the idle watchdog (default 30s) to detect stalled provider connections
- Implement
onFailureto handle rate limits, usage limits, and idle timeouts gracefully - Pass signals through handlers in
open-sse/handlers/chat.tsfor automatic propagation to fetch
Frequently Asked Questions
How do I cancel an in-progress SSE request in OmniRoute?
Create an AbortController, pass its signal property to any OmniRoute handler that accepts it (such as fetchChatCompletions), then call controller.abort() when you need to cancel. The handler forwards this signal to the underlying fetch, and the SSE pipeline handles cleanup automatically.
What HTTP status code does OmniRoute return when an SSE stream is aborted?
It depends on timing. If the abort occurs before any data is received, OmniRoute returns HTTP 499 (client closed request). If the abort happens after partial data has streamed, it returns HTTP 504 (gateway timeout). This distinction helps callers understand whether to retry or treat the request as definitively cancelled.
How does OmniRoute handle provider-side rate limits during SSE streaming?
The createSSEStream transform recognizes response.failed events from providers and forwards them to your onFailure callback. You can inspect the error code (e.g., rate_limit_exceeded) and return true to swallow the error gracefully, or false to let the stream error out. The tests in stream-utils.test.ts demonstrate both patterns.
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 →