can1357/oh-my-pi · error · Error
Legacy SSE endpoint timeout after ${timeout}ms
Error message
Legacy SSE endpoint timeout after ${timeout}ms What it means
After issuing the SSE GET, connect() awaits endpointReady, which only resolves when the stream delivers the 'endpoint' event. If that does not happen within the configured timeout, the operation aborts the fetch and connect() rethrows as 'Legacy SSE endpoint timeout after Xms'. This guards against servers that accept the connection but never send the required handshake event, leaving connect() hanging forever.
Source
Thrown at packages/coding-agent/src/mcp/transports/sse.ts:104
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
if (!response.body) {
throw new Error("Legacy SSE response did not include a body");
}
void this.#readSSEStream(response.body, operation, endpointReady).finally(() => {
const wasConnected = this.#connected;
if (this.#sseConnection === connection) this.#sseConnection = null;
if (wasConnected) this.onClose?.();
});
await endpointReady.promise;
} catch (error) {
operation.clear();
if (this.#sseConnection === connection) this.#sseConnection = null;
connection.abort();
if (operation.isTimeoutAbort(error)) {
throw new Error(`Legacy SSE endpoint timeout after ${timeout}ms`);
}
throw error;
}
}
async #readSSEStream(
body: ReadableStream<Uint8Array>,
operation: MCPTimeoutOperation,
endpointReady: PromiseWithResolvers<void>,
): Promise<void> {
const signal = operation.signal ?? getNeverAbortSignal();
let endpointReceived = false;
try {
for await (const event of readSseEvents(body, signal)) {
if (event.event === "endpoint") {
if (!this.#endpointUrl) {
const endpointUrl = new URL(event.data, this.#config.url);
const configuredUrl = new URL(this.#config.url);View on GitHub (pinned to 9690622007)
Solutions
- Increase the MCP timeout configuration if the server is merely slow to send its endpoint event.
- Test the endpoint manually with curl -N and check that an 'event: endpoint' line arrives promptly; if not, the server (or a proxy) is at fault.
- Restart or fix the MCP server process — a wedged handler won't emit the handshake.
- Check intermediary/proxy buffering and idle timeouts on the streaming route.
- Switch to the Streamable HTTP transport if the server supports it, avoiding the legacy handshake entirely.
Example fix
// before
const client = await createMcpClient({ url: 'https://mcp.example.com/sse', timeout: 3000 });
// after: allow slow legacy servers to complete the handshake
const client = await createMcpClient({ url: 'https://mcp.example.com/sse', timeout: 30000 }); Defensive patterns
Strategy: fallback
Validate before calling
// probe that the endpoint emits its handshake promptly
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);
const res = await fetch(sseUrl, { headers: { Accept: 'text/event-stream' }, signal: ctrl.signal });
// (optionally read the first event and confirm 'event: endpoint')
await res.body?.cancel(); Type guard
function isSseHandshakeTimeout(e: unknown): boolean {
return e instanceof Error && /Legacy SSE endpoint timeout after \d+ms/.test(e.message);
} Try / catch
try {
transport = await createSseTransport(config);
} catch (e) {
if (isSseHandshakeTimeout(e)) {
logger.warn('Legacy SSE handshake timed out; falling back to streamable HTTP');
transport = await createHttpTransport(config);
} else throw e;
} Prevention
- Set timeouts above the server's worst-case handshake latency.
- Monitor for wedged MCP server processes and restart them.
- Disable proxy buffering/idle reaping on streaming routes.
- Prefer Streamable HTTP, which has no endpoint-event handshake.
When it happens
Trigger: Server opens the SSE stream but never sends an 'endpoint' event (incomplete or broken legacy-SSE implementation); server is up but its MCP handler is wedged; an intermediary holds the connection open without forwarding data; timeout configured too low for a slow server.
Common situations: Half-broken proxies or load balancers establishing the TCP stream but stalling the first bytes; server worker threads blocked by another long tool call; legacy SSE servers behind auth middleware that silently swallows the request; very low client timeout values.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- HTTP ${response.status}: ${text}
- Legacy SSE response did not include a body
- Legacy SSE endpoint origin mismatch: expected ${configuredUr
- SSE stream ended without a resumable event ID
- HTTP ${response.status} resuming MCP SSE stream: auth refres
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/6a437a03f3b8def1.
Report an issue: GitHub.