Skip to content

Streamable HTTP client retries the standalone GET after a 401, 403, or 405 #3592

Open

Description

@Jmales

On mcp 1.28.1 through 2.2.0, StreamableHTTPTransport.handle_get_stream treats a 401, 403, or 405 on the standalone server-push GET like a dropped stream. raise_for_status() raises, the except Exception increments the attempt, and the loop sleeps DEFAULT_RECONNECTION_DELAY_MS before trying again, up to MAX_RECONNECTION_ATTEMPTS. Those statuses will not change within the session, so the second GET repeats the first.

Since 1.30.0 the same method already returns immediately when a redirect leaves the endpoint's origin, because retrying that GET cannot help. A status rejection is the same kind of case.

Can we either stop retrying when the server rejects that GET, or add a flag on streamable_http_client to skip subscribing to the server-push stream, the way terminate_on_close=False skips the session DELETE.

Activity

  1. Ben-Home commented on Sep 28, 2026

    @Ben-Home

    Solid analysis. The redirect precedent from 1.30.0 is the right model: some GET outcomes are terminal for the stream, and retrying them only burns the reconnect budget while delaying the session.

    A few points:

    1. Status semantics: 401 and 403 are auth-terminal, and 405 means the server does not implement the standalone GET stream at all. None of these change within a session, so the current loop performs MAX_RECONNECTION_ATTEMPTS identical requests with DEFAULT_RECONNECTION_DELAY_MS sleeps between them, holding up session startup for the full backoff window over a stream that can never start.

    2. On 401 specifically, consider whether a token refresh is possible before giving up on the stream. If the transport has a refresh hook, one retry after a successful refresh is defensible; otherwise treat 401 like 403.

    3. Design options:

      • Return immediately on 401/403/405, mirroring the existing redirect-origin check.
      • Or add a client flag like subscribe_to_server_push=False that skips the standalone GET entirely, matching the terminate_on_close opt-out pattern. Servers used for request/response only (the common tool-calling case) should not pay reconnect cost for a push stream they do not consume.
    4. Also worth documenting what happens when the server returns 405 but the client only wanted the response flow: today that produces silent retries, which is exactly the confusion this issue describes.

  2. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 28, 2026
  3. avinash-git-24 commented on Sep 28, 2026

    @avinash-git-24

    Hey team, handle_get_stream in src/mcp/client/streamable_http.py catches 401/403/405 HTTPStatusErrors in the broad except Exception: retry loop instead of returning early. Happy to open a PR with a fix and test!

  4. junknewera commented on Oct 13, 2026

    @junknewera

    @avinash-git-24 are you still planning to send a PR? If so, I'm happy to step back and review it; otherwise I have a fix with tests ready

  5. avinash-git-24 commented on Oct 13, 2026

    @avinash-git-24

    Hi @junknewera, please feel free to go ahead and open your PR with the tests. Appreciate you checking in!

  6. junknewera commented on Oct 13, 2026

    @junknewera

    Reproduced on main: a 401/403/405 on the standalone GET falls into the except Exception retry loop, so the client waits 1s and sends the same GET again. That can't help: 405 means there's no GET stream, and 401/403 come after the OAuth provider already tried to refresh.

    I have a fix locally (option a): return before raise_for_status() for these statuses, the same way handle_get_stream already returns on a cross-origin redirect. No new API, parametrized test included, full suite plus ruff and pyright pass.

    One question: debug or warning log for 401/403?

    @avinash-git-24 is fine with me taking it (see above), so could a maintainer assign it to me?

  7. mdrxy commented on Oct 27, 2026

    @mdrxy

    Hitting this in practice: the Slack (mcp.slack.com/mcp) and Notion (mcp.notion.com/mcp) servers both return 405 on the GET, so every session logs a traceback plus the "reconnecting" line before it gives up. Tools work fine, it's just noise.

    405 is the spec's way of saying "no SSE stream here" (transports), so it shouldn't be retried or logged as an error.

    +1 to option (a). On the log level question, I'd make 405 a plain debug line since it's expected, and warn on 401/403 since those usually mean bad creds.

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

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions