fix(mcp): detach SSE keep-alive so it cannot wedge the HTTP server

The MCP HTTP server serves connections sequentially on the single
current-thread runtime (the browser session is !Send). The SSE branch
(GET /mcp, Accept: text/event-stream) ran an infinite keep-alive ping loop
inline, so handle_connection never returned and the accept loop never ran
again — one open SSE stream hung every later POST /mcp tool call
indefinitely (functional break + trivial unauthenticated DoS).

The keep-alive carries no browser state and its write half is Send + 'static,
so spawn the ping loop on its own task and return from the handler. POST
handling stays serialized (correct for a single browser session), but a live
SSE stream no longer blocks the accept loop.

Fixes #489
This commit is contained in:
Andrey Mnatsakanov committed 2026-07-26 11:05:57 +02:00
1 parent e314b9b0f3
commit ea417bcd11
2 files changed
+114 -8

No files matched your search

+17 -8
View File
@@ -169,7 +169,14 @@ async fn handle_connection(
}
"GET" if accept_sse => {
// SSE stream: hold open and send periodic keep-alive comments
// SSE stream: hold open and send periodic keep-alive comments.
// Connections are served sequentially (the browser session is
// `!Send`), so holding this infinite keep-alive loop inline would
// never return to the accept loop and would wedge every later
// request. The keep-alive touches no browser state and the write
// half is `Send + 'static`, so detach the ping loop onto its own
// task and return — the accept loop stays free while the stream
// lives on independently.
let hdr = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: text/event-stream\r\n\
@@ -179,14 +186,16 @@ async fn handle_connection(
\r\n"
);
writer.write_all(hdr.as_bytes()).await?;
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(15)).await;
if writer.write_all(b": ping\n\n").await.is_err() {
break;
tokio::spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(15)).await;
if writer.write_all(b": ping\n\n").await.is_err() {
break;
}
let _ = writer.flush().await;
}
let _ = writer.flush().await;
}
break;
});
return Ok(());
}
"POST" => {