{"record":{"id":"63e4539951bd9d3c","repo":"can1357/oh-my-pi","slug":"sse-response-did-not-include-a-body","errorCode":null,"errorMessage":"SSE response did not include a body","messagePattern":"SSE response did not include a body","errorType":"exception","errorClass":"MCPTransportError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/transports/http.ts","lineNumber":543,"sourceCode":"\t\t\t\t\tstage,\n\t\t\t\t\tfailure: \"timeout\",\n\t\t\t\t\tmessage: `Request timeout after ${timeout}ms`,\n\t\t\t\t\tretryable: false,\n\t\t\t\t\ttraceId,\n\t\t\t\t\tcause: error,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (error instanceof Error && error.name === \"AbortError\") throw error;\n\t\t\tthrow normalizeMCPTransportError(error, { transport: \"http\", stage, traceId });\n\t\t} finally {\n\t\t\toperation.clear();\n\t\t}\n\t}\n\n\t#parseSSEResponse<T>(response: Response, expectedId: string | number, options?: MCPRequestOptions): Promise<T> {\n\t\tconst traceId = mcpTraceIdFromHeaders(response.headers);\n\t\tif (!response.body) {\n\t\t\tthrow new MCPTransportError({\n\t\t\t\ttransport: \"http\",\n\t\t\t\tstage: \"decode\",\n\t\t\t\tfailure: \"malformed_response\",\n\t\t\t\tmessage: \"SSE response did not include a body\",\n\t\t\t\tretryable: false,\n\t\t\t\ttraceId,\n\t\t\t});\n\t\t}\n\n\t\tconst timeout = resolveMCPTimeoutMs(this.config.timeout);\n\t\tconst operation = createMCPTimeout(timeout, this.#operationSignal(options?.signal));\n\t\tconst signal = operation.signal ?? getNeverAbortSignal();\n\n\t\tconst { promise, resolve, reject } = Promise.withResolvers<T>();\n\t\t// The transport owns this promise until the physical stream drain exits.\n\t\t// Keep a rejection observer attached even when a caller-side timeout\n\t\t// abandons the request before the drain notices its abort.\n\t\tvoid promise.catch(() => {});","sourceCodeStart":525,"sourceCodeEnd":561,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/transports/http.ts#L525-L561","documentation":"The Streamable HTTP transport POSTed a JSON-RPC request and got a 2xx response with Content-Type text/event-stream, but the Response object had a null body, so there is no SSE stream to parse. The transport treats this as a malformed server response because a valid SSE reply must carry a readable body stream. It throws immediately from #parseSSEResponse before any events are read.","triggerScenarios":"Server returns 200/202 with Content-Type: text/event-stream but a null body (e.g. a broken proxy or adapter that swallowed the stream); a misconfigured middleware that strips the body while keeping the SSE content-type header.","commonSituations":"Corporate proxies or API gateways (e.g. nginx buffering misconfig, Cloudflare workers) that return an empty streaming response; buggy MCP server implementations that set the SSE content-type without piping the event stream; serverless wrappers that buffer responses to zero bytes.","solutions":["Inspect the raw server response with curl -N -H 'Accept: text/event-stream' to confirm the server really sends an empty SSE stream; fix or update the MCP server if so.","Check and fix any proxy/gateway in front of the MCP server that may strip or buffer the response body (disable response buffering, e.g. proxy_buffering off in nginx).","Update the MCP server to the latest version — returning an SSE content-type with no body violates the Streamable HTTP spec.","Fall back to a different transport (stdio or legacy SSE) for this server if its HTTP implementation is broken.","Since the error is marked retryable:false, do not blindly retry; capture mcpTraceIdFromHeaders trace id from server logs when reporting."],"exampleFix":"// server (before): sets header but forgets to stream\nres.setHeader('Content-Type', 'text/event-stream');\nres.end();\n// after: keep the stream open and write events\nres.setHeader('Content-Type', 'text/event-stream');\nres.write(`event: message\\ndata: ${JSON.stringify(reply)}\\n\\n`);","handlingStrategy":"try-catch","validationCode":"const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body });\nif (res.headers.get('content-type')?.includes('text/event-stream') && !res.body) {\n  throw new Error('Server returns SSE content-type with no body — server/proxy is broken');\n}","typeGuard":"function hasBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {\n  return res.body !== null;\n}","tryCatchPattern":"try {\n  await client.request('tools/list');\n} catch (e) {\n  if (e instanceof MCPTransportError && e.failure === 'malformed_response' && e.stage === 'decode') {\n    logger.error('MCP server returned a body-less SSE stream', { traceId: e.traceId });\n    // do not retry: e.retryable === false; fall back to stdio transport\n  } else throw e;\n}","preventionTips":["Smoke-test new MCP servers with curl -N before wiring them in.","Disable response buffering on proxies for SSE routes.","Prefer the Streamable HTTP transport and current server versions.","Avoid hosting streaming MCP endpoints on runtimes without streaming response support."],"tags":["mcp","http","sse","malformed-response"],"backgroundTag":"empty-response-body","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}