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

  1. Ensure the MCP server includes `id:` fields on SSE events so resumption is possible (required for streamability per the MCP spec)
  2. Treat this as a logical stream end: the transport's onClose/reconnect path handles it — verify server liveness and reconnect via normal connect flow
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/ad2899a37628e3e6. Report an issue: GitHub.