can1357/oh-my-pi · warning · SSEResumeError
SSE stream ended without a resumable event ID
Error message
SSE stream ended without a resumable event ID
What it means
The long-lived GET SSE listener dropped without ever delivering an event carrying an `id:` field, so the transport cannot resume the logical stream with a Last-Event-ID header. It throws SSEResumeError to end the stream cleanly so `request()` never replays the original POST (which the server may have already executed).
Source
Thrown at packages/coding-agent/src/mcp/transports/http.ts:342
logger.debug("HTTP SSE listener resume failed", {
url: this.config.url,
error: error instanceof Error ? error.message : String(error),
});
}
return;
}
}
}
/**
* Resume a logical SSE stream via GET + `Last-Event-ID`, honoring the
* server-provided retry interval and refreshing auth once on 401/403.
* Failures throw {@link SSEResumeError} so `request()` never replays the
* originating POST in response.
*/
async #fetchSSEResume(resume: SSEResumeState, signal: AbortSignal): Promise<Response> {
if (resume.lastEventId === null) {
throw new SSEResumeError("SSE stream ended without a resumable event ID");
}
await waitForSSERetry(resume.retryMs, signal);
const generated: Record<string, string> = {
Accept: "text/event-stream",
"Last-Event-ID": resume.lastEventId,
};
if (this.#sessionId) generated["Mcp-Session-Id"] = this.#sessionId;
let response = await this.#fetch({ method: "GET", signal }, generated);
if (this.onAuthError && (response.status === 401 || response.status === 403)) {
await response.body?.cancel();
const newHeaders = await this.onAuthError();
if (!newHeaders) {
throw new SSEResumeError(`HTTP ${response.status} resuming MCP SSE stream: auth refresh failed`);
}
// Persist refreshed headers so subsequent requests use them directly
this.config = { ...this.config, headers: newHeaders };
response = await this.#fetch({ method: "GET", signal }, generated);
}View on GitHub (pinned to 9690622007)
Solutions
- Ensure the MCP server includes `id:` fields on SSE events so resumption is possible (required for streamability per the MCP spec)
- Treat this as a logical stream end: the transport's onClose/reconnect path handles it — verify server liveness and reconnect via normal connect flow
- Check network stability between client and server; a proxy or idle timeout may be cutting the connection before events arrive
Defensive patterns
Strategy: fallback
Try / catch
transport.onError = (err) => {
if (err.message === "SSE stream ended without a resumable event ID") {
// logical stream ended; schedule a full reconnect instead of expecting resume
scheduleReconnect();
}
}; Prevention
- Ensure the MCP server emits id: fields on SSE events
- Monitor connection stability; dropouts before the first event mean resume is impossible
- Treat this as a reconnect signal, not a retryable request failure
- Keep sessions short on flaky networks
When it happens
Trigger: The physical SSE connection breaks (network drop, server restart) while resume.lastEventId is still null — i.e. the server sent no id-tagged events before the stream ended.
Common situations: Connecting to an MCP server that does not emit event IDs on its stream; a very short-lived connection that dies during initialization; a server that closes the GET stream immediately after responding.
Related errors
- V2 compaction stream closed before response.completed
- stream ended before message_start
- Perplexity ask stream error: ${message}
- V2 compaction stream parse failed: ${err instanceof Error ?
- Auth broker stream did not start with snapshot
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/ad2899a37628e3e6.
Report an issue: GitHub.