Skip to content

HttpServletStreamableServerTransportProvider: server-initiated requests race client stream registration (intermittent -32603 "Stream unavailable") #1114

Closed

Description

@slachiewicz

Summary

HttpServletStreamableServerTransportProvider intermittently answers -32603 "Stream unavailable for session <id>" (wrapped as JSON-RPC internal error / HTTP 500) when it attempts to push a server-initiated request (e.g. roots/list) to a client whose GET /mcp SSE stream has not been registered yet.

Observed failure

  • Test: HttpServletStreamableIntegrationTests.testRootsNotificationWithEmptyRootsList(String)[1]
  • CI run: https://github.com/modelcontextprotocol/java-sdk/actions/runs/33068920136 (Build and Test)
  • Client-side surfacing: java.lang.RuntimeException: Failed to send message: AggregateResponseEvent[...] wrapping
    jsonRpcError: {"code":-32603,"message":"Error processing message: Stream unavailable for session 82454020-4c10-4276-9c30-57c15d49810e"}

Repro shape

  1. Client declares the roots capability
  2. Client sends initialize; server accepts and creates the session
  3. Server immediately pushes roots/list over the session's GET /mcp stream
  4. POST traffic racing the first stream registration loses → -32603, stream unavailable

Why we believe it is a race (not environment-specific)

Suggested directions

  1. Buffer server→client messages per session until a stream registers (short grace window), or
  2. Single retry on "stream unavailable" for capability-prompted pushes

A test-side await cannot fix this, since the failure occurs inside the server's push path.

Environment

Activity

  1. added
    bugSomething isn't working
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    needs reproWe need a way to reproduce the problem
    on Aug 28, 2026
  2. Kehrlann commented on Aug 28, 2026

    @Kehrlann
    Contributor

    Unable to reproduce locally, even running the tests 1000 times. It might not be the exact problem described here. The test in question does the following:

    1. client initializes fully
      1. POST /initialize`
      2. GET /mcp (opens a stream), in a fire-and-forget
      3. POST notifications/initialized
    2. client notify roots lists changes POST notications/roots/list_changed
    3. Only when the server gets the notification does the server sends roots/list.

    It is very unlikely that the GET is under contention for two full posts. I can reproduce by adding latency to the servlet transport's doGet, though.

    The underlying issue (race condition between server notifications and GET) might be a problem under some conditions (network, client from a different ecosystem).

    (There is also a separate issue at play here - the client can't really wait for the stream to be established before proceeding further, which would fix the test without fixing the race condition. But that's a whole other can of worms.)

  3. Kehrlann commented on Aug 28, 2026

    @Kehrlann
    Contributor

    Please let's discuss server-side fixes in this issue before jumping into an implementation.

  4. added theissue type on Aug 28, 2026
  5. slachiewicz commented on Aug 28, 2026

    @slachiewicz
    ContributorAuthor

    Fresh occurrence in CI, on a branch that only touches a test file: Jackson 2 job of #1113 at 8fccf33, HttpServletStreamableIntegrationTests.testRootsSuccess(String)[1] → -32603 "Error processing message: Stream unavailable for session f79d57db-…" from HttpServletStreamableServerTransportProvider.doPost:564 (run 33192309921). Same seam as testRootsNotificationWithEmptyRootsList; the sibling Jackson 3 job passed the same class.

    This comment was created with AI assistance.

  6. CryoThrust commented on Aug 31, 2026

    @CryoThrust

    I can help investigate this race. The current discussion suggests the server-side contract needs to distinguish a session that exists from a stream that is not registered yet. Before proposing code, would maintainers prefer buffering pending server-to-client messages per session (with bounded lifetime) or a retryable send result? I can prepare a deterministic delayed-GET integration test once the preferred invariant is confirmed.

  7. Kehrlann commented on Aug 31, 2026

    @Kehrlann
    Contributor

    Looking around at what the other SDKs do, CSharp, TypeScript and Python suffer the same race condition and don't try to buffer or replay. I don't plan on changing the current behavior, especially with the new spec around the corner that has most of our focus.

    I'm open to stabilizing the tests though.
    The issue is with server-sent notifications, so possibly roots, elicitiation and sampling. They seem to mostly be happening in roots, so I'll add the explicit await there.

  8. slachiewicz commented on Aug 31, 2026

    @slachiewicz
    ContributorAuthor

    Fine with stabilizing the tests and leaving the server behavior as-is, given parity with the C#/TS/Python SDKs and the upcoming spec revision. Since #1119 does not change the runtime race, I would suggest closing this as not-planned rather than via "Fixes", so the decision stays discoverable when the 2026-07-28 transport work touches this seam.

    This comment was created with AI assistance.

  9. added 2 commits that reference this issue on Sep 1, 2026
    7b7eab8
    3785d3c
  10. added a commit that references this issue on Sep 1, 2026
    5931625
  11. added
    wontfixThis will not be worked on
    and removed
    needs reproWe need a way to reproduce the problem
    on Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurearea/serverbugSomething isn't workingwontfixThis will not be worked on

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions