Skip to content

Streamable HTTP: pre-existing hardening items from the #2547 review #2562

Open

Description

@felixweinberger

Three pre-existing Streamable HTTP transport items surfaced during the #2547 review that are independent of the keep-alive and delivery work there. Collecting them here so they don't get lost; each is small enough to fix individually.

  1. Protocol-version gate accepts unvalidated strings on initialize. The priming/resumability gate compares protocolVersion >= '2025-11-25' lexicographically, and for initialize requests the string comes verbatim from the request body without being checked against SUPPORTED_PROTOCOL_VERSIONS (non-initialize requests are header-validated). Unsupported strings that sort above the threshold (draft, 2026-01-01) enable priming events and the polling callbacks for the initialize stream even though the negotiated version in the response may be lower. Gate on the negotiated version instead.

  2. Concurrent DELETE requests can fire onsessionclosed twice. While the first DELETE is parked on the callback await, _closed is still false, so a second DELETE passes the guards and fires the callback again for the same session. A flag set before invoking the callback closes the window.

  3. The EventStore contract has no session scoping for the standalone stream. Every transport stores standalone server-initiated messages under the fixed id _GET_stream, so a store instance shared across sessions (module-level store in ssePollingExample, or any multi-node shared store) mixes sessions on standalone resume: one session's resume can replay another session's notifications. The contract needs a session dimension, or documented single-session store usage.

Applies to both main and v1.x.

Activity

  1. axits-lab commented on Jul 30, 2026

    @axits-lab

    @felixweinberger I'd like to take these — starting with item 2 (concurrent DELETE double-firing onsessionclosed), then item 1 (gate on the negotiated version rather than the unvalidated protocolVersion string from the initialize body), as separate PRs since you noted each stands alone.

    Item 3 reads like it needs a decision before code: adding a session dimension to the EventStore contract is a breaking change for implementors, whereas documenting single-session store usage isn't. Happy to take it once you've called which way it should go.

    Are you already on any of these? Don't want to duplicate your own follow-up work.

  2. axits-lab commented on Jul 30, 2026

    @axits-lab

    On item 1 — I think this one is mostly overtaken by #2280, and the remainder isn't local. Flagging before writing code rather than guessing at intent.

    The described failure (draft, 2026-01-01 sorting above the threshold and enabling priming) is already blocked on main: #2280 bounded the gate to supported versions on 2026-06-12, so supportsEmptySSEData is

    return this._supportedProtocolVersions.includes(protocolVersion) && protocolVersion >= '2025-11-25';

    An unsupported string fails includes() and never reaches the comparison, for both the priming event and the closeSSEStream/closeStandaloneSSEStream callbacks.

    What's left is the last sentence — gating on the negotiated version — and that isn't a small change, because the negotiated version doesn't exist yet at the point the gate runs. writePrimingEvent is called at streamableHttp.ts:938, while initialize isn't handed to the server until the onmessage dispatch at :957. So the transport only ever has initRequest.params.protocolVersion (what the client asked for) to work with. Getting the negotiated value would mean deferring the priming event until after the InitializeResult is produced, which reshapes the initialize stream lifecycle rather than tightening a condition.

    There is still a real gap, but it's the inverse of the one described: when a client requests a version this transport doesn't support, includes() is false and priming stays off even though the negotiated version may well be >= 2025-11-25 and support it. That's a lost capability, not an unsafe enable.

    So — do you want (a) item 1 closed as covered by #2280, (b) the false-negative above fixed by gating on the version the transport would negotiate (computable pre-dispatch from _supportedProtocolVersions), or (c) the full deferral so the gate sees the true negotiated version? Happy to take (b) or (c); (c) is big enough that I'd want your call on the shape first.

    Item 2 is up as #2583 (draft — the one-open-PR cap for non-write-access users, will mark ready once #2582 lands).

  3. added
    bugSomething isn't working
    ready for workEnough information for someone to start working on
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Aug 17, 2026
  4. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    v1Issues / PRs related to v1.x
    on Aug 18, 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 featurebugSomething isn't workingready for workEnough information for someone to start working onv1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions