Skip to content

fix: observe endpointstream shutdown in SSE closed() - #68

Merged
ruoka merged 1 commit into
masterfrom
cursor/critical-bug-management-c7b0
Aug 2, 2026
Merged

fix: observe endpointstream shutdown in SSE closed()#68
ruoka merged 1 commit into
masterfrom
cursor/critical-bug-management-c7b0

Conversation

@cursor

@cursor cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Bug and impact

After drain-on-listen-exit, http::server::stop() calls endpointstream::shutdown() on live clients and then wait_for_handlers(). shutdown() wakes blocking I/O but does not set iostream failbit until the next write.

http::sse::session::closed() only checked m_closed || !stream.good(). Custom SSE/MCP handlers that only poll closed() (including the MCP inbound-queue tests that hold a session open with while (!sess.closed())) never observe stop(), so listen() / join hang forever.

Production mcp::server::run_session already probes with heartbeats and is less affected; any attach handler that only polls closed() deadlocks shutdown.

Root cause

stop()endpointstream::shutdown() does not surface as stream failure until a write. Session lifecycle state (m_state->closed) is only set in erase_session after on_session returns, so a poll-only handler has no signal.

Fix

  • Latch shut_down on endpointbuf_base::shutdown()
  • Expose it via endpointstream::shut_down()
  • Have sse::session::closed() observe the latch (via dynamic_cast to endpointstream)

Validation

  • Added unit section: closed after endpointstream shutdown latch
  • Ran filtered tests matching shutdown latch|MCP SSE|SSE session (includes inbound-queue 503 scenarios that previously hung on stop()): all passed
Open in Web View Automation 

Note

Medium Risk
Touches connection lifecycle and SSE shutdown visibility; limited scope but affects server stop and MCP/SSE handler exit paths.

Overview
Fixes a shutdown deadlock where http::server::stop() shuts down client endpointstreams but handlers that only poll http::sse::session::closed() never saw closure until a write failed.

endpointbuf_base::shutdown() now sets an atomic shut_down latch (in addition to waking the socket). endpointstream::shut_down() exposes that state (including after close()). sse::session::closed() treats an underlying endpointstream as closed when the latch is set, via dynamic_cast.

Adds a regression test: after stream.shutdown(), session.closed() is true while iostream good() may still be true.

Reviewed by Cursor Bugbot for commit ff2d785. Bugbot is set up for automated code reviews on this repo. Configure here.

stop() shuts down live client sockets but does not set iostream failbit
until the next write. MCP handlers that only poll session.closed() (including
the inbound-queue tests) never exited, so wait_for_handlers() deadlocked after
drain-on-listen-exit. Latch shut_down on shutdown() and check it from
sse::session::closed().

Co-authored-by: Kaius  Ruokonen <ruoka@users.noreply.github.com>
@ruoka
ruoka force-pushed the cursor/critical-bug-management-c7b0 branch from f16cdd5 to ff2d785 Compare August 2, 2026 01:36
@ruoka
ruoka marked this pull request as ready for review August 2, 2026 01:36
@ruoka
ruoka merged commit cc3dc08 into master Aug 2, 2026
@ruoka
ruoka deleted the cursor/critical-bug-management-c7b0 branch August 2, 2026 01:37
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