{"record":{"id":"6a437a03f3b8def1","repo":"can1357/oh-my-pi","slug":"legacy-sse-endpoint-timeout-after-timeout-ms","errorCode":null,"errorMessage":"Legacy SSE endpoint timeout after ${timeout}ms","messagePattern":"Legacy SSE endpoint timeout after (.+?)ms","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/transports/sse.ts","lineNumber":104,"sourceCode":"\t\t\t\tconst text = await response.text();\n\t\t\t\tthrow new Error(`HTTP ${response.status}: ${text}`);\n\t\t\t}\n\t\t\tif (!response.body) {\n\t\t\t\tthrow new Error(\"Legacy SSE response did not include a body\");\n\t\t\t}\n\n\t\t\tvoid this.#readSSEStream(response.body, operation, endpointReady).finally(() => {\n\t\t\t\tconst wasConnected = this.#connected;\n\t\t\t\tif (this.#sseConnection === connection) this.#sseConnection = null;\n\t\t\t\tif (wasConnected) this.onClose?.();\n\t\t\t});\n\t\t\tawait endpointReady.promise;\n\t\t} catch (error) {\n\t\t\toperation.clear();\n\t\t\tif (this.#sseConnection === connection) this.#sseConnection = null;\n\t\t\tconnection.abort();\n\t\t\tif (operation.isTimeoutAbort(error)) {\n\t\t\t\tthrow new Error(`Legacy SSE endpoint timeout after ${timeout}ms`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync #readSSEStream(\n\t\tbody: ReadableStream<Uint8Array>,\n\t\toperation: MCPTimeoutOperation,\n\t\tendpointReady: PromiseWithResolvers<void>,\n\t): Promise<void> {\n\t\tconst signal = operation.signal ?? getNeverAbortSignal();\n\t\tlet endpointReceived = false;\n\t\ttry {\n\t\t\tfor await (const event of readSseEvents(body, signal)) {\n\t\t\t\tif (event.event === \"endpoint\") {\n\t\t\t\t\tif (!this.#endpointUrl) {\n\t\t\t\t\t\tconst endpointUrl = new URL(event.data, this.#config.url);\n\t\t\t\t\t\tconst configuredUrl = new URL(this.#config.url);","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/transports/sse.ts#L86-L122","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst client = await createMcpClient({ url: 'https://mcp.example.com/sse', timeout: 3000 });\n// after: allow slow legacy servers to complete the handshake\nconst client = await createMcpClient({ url: 'https://mcp.example.com/sse', timeout: 30000 });","handlingStrategy":"fallback","validationCode":"// probe that the endpoint emits its handshake promptly\nconst ctrl = new AbortController();\nsetTimeout(() => ctrl.abort(), 5000);\nconst res = await fetch(sseUrl, { headers: { Accept: 'text/event-stream' }, signal: ctrl.signal });\n// (optionally read the first event and confirm 'event: endpoint')\nawait res.body?.cancel();","typeGuard":"function isSseHandshakeTimeout(e: unknown): boolean {\n  return e instanceof Error && /Legacy SSE endpoint timeout after \\d+ms/.test(e.message);\n}","tryCatchPattern":"try {\n  transport = await createSseTransport(config);\n} catch (e) {\n  if (isSseHandshakeTimeout(e)) {\n    logger.warn('Legacy SSE handshake timed out; falling back to streamable HTTP');\n    transport = await createHttpTransport(config);\n  } else throw e;\n}","preventionTips":["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."],"tags":["mcp","sse","timeout","handshake","legacy-transport"],"backgroundTag":"handshake-timeout","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}