Skip to content

fix(transport): avoid stall on servers with idle standalone SSE streams#234

Merged
steipete merged 3 commits into
openclaw:mainfrom
umutkeltek:fix/streamable-http-idle-sse-stall
Jul 21, 2026
Merged

fix(transport): avoid stall on servers with idle standalone SSE streams#234
steipete merged 3 commits into
openclaw:mainfrom
umutkeltek:fix/streamable-http-idle-sse-stall

Conversation

@umutkeltek

@umutkeltek umutkeltek commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Problem

Streamable HTTP servers may hold the standalone GET SSE stream open indefinitely without emitting bytes. That is a spec-legal server-to-client notification channel, but affected Node fetch pools can let the idle response occupy the origin pool and queue later same-origin requests forever. In practice, tools/list and tool calls time out even though the server is healthy.

@umutkeltek traced this against Paddle at the wire level: initialization and the idle SSE GET completed, but the following POST never reached the server. The same behavior reproduced without the MCP SDK, while separate HTTP connections continued to work.

Fix

  • Route only standalone SSE GET requests through mcporter's separate Node HTTP/1.1 connection path.
  • Keep ordinary request/response traffic on the default fetch implementation.
  • Preserve explicit httpFetch: "default" and httpFetch: "node-http1" behavior, plus the existing all-request Sunsama compatibility path.
  • Leave Bun on its native fetch stack; the affected pool behavior and compatibility path are Node-specific.

This fixes the server class generically instead of adding a Paddle hostname exception.

Verification

  • Added a built-CLI integration test with a real loopback MCP server, a byte-idle standalone SSE response, and a single-connection default fetch pool. Current main times out after the idle GET; this branch sends tools/list while that stream remains open and exits cleanly.
  • pnpm exec vitest run tests/node-http-fetch.test.ts tests/runtime-transport.test.ts tests/cli-idle-sse.integration.test.ts tests/generate-cli.test.ts — 44 passed.
  • pnpm check — passed.
  • pnpm test — 837 passed, 3 skipped.
  • pnpm docs:site — passed.
  • Canonical AutoReview on the final branch diff — clean, no accepted/actionable findings.
  • GitHub Actions run 29793828251 — macOS, Windows, and Ubuntu jobs passed on exact head c5e17d3936a5a3e1549c4c503cc038bd1899dbfe.

Contributor credit is preserved in the original commit and maintainer follow-ups.

umutkeltek and others added 3 commits July 20, 2026 23:33
Streamable HTTP servers may hold the standalone GET SSE stream open
indefinitely without emitting any bytes. That is a spec-legal idle
server-to-client notification channel, but under undici the open response
occupies the connection for its origin and every subsequent same-origin
request queues behind it forever, so tools/list and all tool calls hang
until they time out.

mcp.paddle.com behaves this way: initialize and notifications/initialized
succeed, the GET stream returns 200 and then sends nothing, and the next
POST never completes. `mcporter list paddle-*` timed out at any timeout
value while curl against the same endpoint returned in ~0.2s.

Reproduced with plain fetch and no SDK involved. The block is
origin-scoped (requests to other origins from the same process are
unaffected) and occurs whether or not the SSE body is read. Raising
`connections` or enabling `allowH2` on a shared dispatcher does not help;
only a separate connection pool does.

nodeHttp1Fetch already sidesteps the shared pool, and api.sunsama.com was
already special-cased for what looks like the same failure. Generalize
that bare hostname check into a documented constant set and add
mcp.paddle.com, so both work without configuration. Users can still
override per server with `httpFetch`.
Co-authored-by: Umut Keltek <umut.keltek@gmail.com>
Co-authored-by: Umut Keltek <umut.keltek@gmail.com>
@steipete
steipete merged commit 57cf3b1 into openclaw:main Jul 21, 2026
3 checks passed
@steipete

Copy link
Copy Markdown
Collaborator

Landed as 57cf3b19aba60dca5cb2ed003f58d259e93dcd86.

Proof on exact PR head c5e17d3936a5a3e1549c4c503cc038bd1899dbfe:

  • Baseline reproduction: the built CLI reached initialize, notifications/initialized, and a byte-idle standalone SSE GET; tools/list never reached the loopback server and the CLI timed out after 2003 ms.
  • Fixed boundary: the built CLI observed initialize -> notifications/initialized -> GET standalone-sse -> tools/list, with the SSE response still open when tools/list arrived.
  • pnpm exec vitest run tests/node-http-fetch.test.ts tests/runtime-transport.test.ts tests/cli-idle-sse.integration.test.ts tests/generate-cli.test.ts: 44 passed.
  • pnpm check: passed.
  • pnpm test: 837 passed, 3 skipped.
  • pnpm docs:site: passed.
  • Canonical AutoReview on the final branch diff: clean, no accepted/actionable findings.
  • GitHub Actions run 29793828251: macOS job 88521141467, Windows job 88521141468, and Ubuntu job 88521141478 all passed.

The merged implementation isolates only Node's standalone SSE receive channel, preserves ordinary/default fetch traffic and explicit overrides, and keeps Bun on its native fetch stack. @umutkeltek's authorship is preserved via the squash commit co-author trailer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants