can1357/oh-my-pi · error · MCPTransportError

SSE response did not include a body

Error message

SSE response did not include a body

What it means

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.

Source

Thrown at packages/coding-agent/src/mcp/transports/http.ts:543

					stage,
					failure: "timeout",
					message: `Request timeout after ${timeout}ms`,
					retryable: false,
					traceId,
					cause: error,
				});
			}
			if (error instanceof Error && error.name === "AbortError") throw error;
			throw normalizeMCPTransportError(error, { transport: "http", stage, traceId });
		} finally {
			operation.clear();
		}
	}

	#parseSSEResponse<T>(response: Response, expectedId: string | number, options?: MCPRequestOptions): Promise<T> {
		const traceId = mcpTraceIdFromHeaders(response.headers);
		if (!response.body) {
			throw new MCPTransportError({
				transport: "http",
				stage: "decode",
				failure: "malformed_response",
				message: "SSE response did not include a body",
				retryable: false,
				traceId,
			});
		}

		const timeout = resolveMCPTimeoutMs(this.config.timeout);
		const operation = createMCPTimeout(timeout, this.#operationSignal(options?.signal));
		const signal = operation.signal ?? getNeverAbortSignal();

		const { promise, resolve, reject } = Promise.withResolvers<T>();
		// The transport owns this promise until the physical stream drain exits.
		// Keep a rejection observer attached even when a caller-side timeout
		// abandons the request before the drain notices its abort.
		void promise.catch(() => {});

View on GitHub (pinned to 9690622007)

Solutions

  1. 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.
  2. 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).
  3. Update the MCP server to the latest version — returning an SSE content-type with no body violates the Streamable HTTP spec.
  4. Fall back to a different transport (stdio or legacy SSE) for this server if its HTTP implementation is broken.
  5. Since the error is marked retryable:false, do not blindly retry; capture mcpTraceIdFromHeaders trace id from server logs when reporting.

Example fix

// server (before): sets header but forgets to stream
res.setHeader('Content-Type', 'text/event-stream');
res.end();
// after: keep the stream open and write events
res.setHeader('Content-Type', 'text/event-stream');
res.write(`event: message\ndata: ${JSON.stringify(reply)}\n\n`);
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body });
if (res.headers.get('content-type')?.includes('text/event-stream') && !res.body) {
  throw new Error('Server returns SSE content-type with no body — server/proxy is broken');
}

Type guard

function hasBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {
  return res.body !== null;
}

Try / catch

try {
  await client.request('tools/list');
} catch (e) {
  if (e instanceof MCPTransportError && e.failure === 'malformed_response' && e.stage === 'decode') {
    logger.error('MCP server returned a body-less SSE stream', { traceId: e.traceId });
    // do not retry: e.retryable === false; fall back to stdio transport
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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