AgentFixboard

HEAD on the SSE endpoint never finishes and hangs the keep-alive connection

HEAD /sse on MCPServer.sse_app() returns headers and then never ends the response. A second request on the same keep-alive connection hangs. The report's curl printed HEAD=200 then next=000 new_connections=0 t=5.002893s.

From GitHub

modelcontextprotocol/python-sdk

Difficulty
Moderate
Time
~15 min
Steps
3
Fixed in
—
Last checked

Not fixed upstream yet: the issue is open. Source: issue open. Last checked 2026-10-22.

Symptom

HEAD /sse on MCPServer.sse_app() returns headers and then never ends the response. A second request on the same keep-alive connection hangs. The report's curl printed HEAD=200 then next=000 new_connections=0 t=5.002893s.

Evidence what you'll see

  • sse_app() registers the SSE route with methods=["GET"]. Starlette 1.9.0 then adds HEAD. EventSourceResponse has no HEAD handling, so it streams forever.
  • After the client is gone the server can keep the socket: ss -tan '( sport = :8000 )' | grep -c CLOSE-WAIT printed 1 per repeat.
  • LibreChat sends HEAD <url> at startup for OAuth detection. Behind an nginx keep-alive gateway the report saw hung requests and, after three days, 99 CLOSE-WAIT sockets on one Python SSE server.

Why it happens

HEAD runs the full SSE handler (connect_sse()), which opens a session and starts a stream that never ends. uvicorn still waits for more_body=False and can pause reading once a pipelined request arrives, so http.disconnect never comes.

Affected versions

mcp 2.3.0 with sse-starlette 3.5.0, Starlette 1.9.0, uvicorn 0.54.0 (h11 0.16.0), Python 3.13.0. Also seen on mcp 1.28.1. The snippet is uvicorn.run(mcp.sse_app(), host="127.0.0.1", port=8000).

Fix steps

0 of 3 done

Your check marks stay in this browser only.

How to check it worked

  • The report's curl pair then prints HEAD=200 and an immediate status for the next URL on the same connection, with no new CLOSE-WAIT socket.

If it still fails

  • Fixing only EventSourceResponse is not enough: handle_sse then sends a trailing return Response() and uvicorn raises RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.
  • Related HEAD handling on Streamable HTTP is issues #1269 and #2191. sysid/sse-starlette#218 tracks the generic EventSourceResponse hang. A lone HEAD whose client closes immediately is cleaned up; the leak needs the follow-up keep-alive request.

Sources and credit

Summarized in our own words from these public reports. Credit goes to their authors.

Provided as is, without warranty. Try changes in a non-production environment and back up first. Security-related steps: verify against the official docs. Terms · Request removal or attribution change

Hit an error we don't cover? Submit it