Repository navigation
Streamable HTTP notification triggers duplicate response after HTTP 202 #3631
Description
Activity
- addedv2Affects the v2 line (2.x on main)Affects the v2 line (2.x on main)v1Affects the v1.x maintenance lineAffects the v1.x maintenance line
on Oct 2, 2026 I tried the repro against current main (2118f14) and it fails the same way there.
From what I can tell the problem is the order of things in the notification branch of
_handle_post_request. It sends the 202 first and only then callswriter.send(session_message). When the session stream is already closed that send raisesClosedResourceError, and the catch allexcept Exceptionbelow it tries to send a 500 even though the 202 has already gone out. It then writes the error into the same closed stream again, which fails a second time.The fix I tried is small: wrap that one
writer.sendin a try/except foranyio.ClosedResourceErrorandanyio.BrokenResourceErrorand just log at debug level. That's what_streamable_http_modern.pyalready does innotify()when its stream is gone, so it stays consistent with the rest of the SDK. Since the 202 is already out, there isn't anything left to tell the client.I also turned the repro into a test in
tests/shared/test_streamable_http.pythat checks we get exactly one 202. It fails on main with the ExceptionGroup and passes with the change, and the full suite still passes with coverage at 100%. The branch is here if it helps: https://github.com/premanand8800/python-sdk/tree/fix/notification-202-closed-session@lecton-apptio you opened this, so if you'd rather fix it yourself, go ahead. I'm happy either way.
Full disclosure: I used Claude Code to help dig into this and test it, but I went through the change myself and can answer questions about it.
Reproduced on current
main(2118f14f) with the issue's harness: after the 202 response starts,writer.send(session_message)raisesClosedResourceErrorand the generic POST error handler then attempts a second HTTP response, which fails (AssertionErrorfrom the ASGI transport: response already started) — so the client-facing failure is the duplicate response, not just the closed writer.Approach taken locally (additive, no public-API change): in the notification branch of
_handle_post_request, catchClosedResourceErrorfrom thewriter.sendafter the 202 is sent and drop the message with a debug log instead of falling into the generic error handler. That handler both attempts the second response and retrieswriter.send(Exception(err)), so catching at the site covers all three expectations in the issue (no second response, no unhandled exception, no second write to the closed writer). The normal path (open writer) is untouched.A regression test (notification POST with a closed session writer asserts 202 and no escaping exception; it fails on unpatched
main) plus the repo's checks (ruff check,ruff format --check,pyrightclean; existing server streamable suites green) pass. Fix is implemented and tested in a branch on my fork — happy to open a PR if this is assigned or taggedhelp wanted.(Disclosing: prepared with AI assistance; I ran the repro and tests myself and can explain the change.)
I've reproduced this on current main as well — same pair of exceptions as described above: the notification branch sends the 202, then
writer.send(session_message)raisesClosedResourceErroronce the session tears down, and the outer handler attempts a second response on the completed ASGI scope. I have a tested fix for this (regression tests included) from the duplicate #3641 — happy to retarget the PR here. Could you assign #3631 to me?
Initial Checks
Release line
2.x (current stable)
Description
Summary
MCP 2.1.1 can send HTTP 202 for a JSON-RPC notification and then raise
ClosedResourceErrorwhen writing the notification to a closed internal session writer.The outer error handler then attempts to send a second HTTP error response after the 202 response has already started. This can result in an unhandled
ExceptionGroupor duplicate-response error.Environment
notifications/initializedExpected behavior
Once MCP has sent HTTP 202 for a notification, a closed internal session writer should not trigger:
Actual behavior
The notification path currently:
writer.send(session_message).ClosedResourceErrorbecause the writer is closed.This can result in an unhandled
ExceptionGroupor duplicate-response failure.Minimal reproduction
StreamableHTTPServerTransportwith JSON response mode enabled.transport.connect().transport._read_stream_writer.notifications/initializedPOST request throughtransport.handle_requestwith ASGI exceptions enabled.Evidence
The closed writer failure originates in:
mcp/shared/_context_streams.py:37The notification handler sends HTTP 202 before attempting to write the notification to the internal session stream.
Related issues
This is related to the closed-writer handling discussed in #2064, but the failure occurs on the MCP v2 notification path.
Temporary workaround
I added a version-specific safeguard that:
ClosedResourceErrorfrom escaping.The SDK still emits the initial
Error handling POST requestlog because that logging occurs before the workaround can intercept the secondary failure.Example Code
Python & MCP Python SDK