Skip to content

Document SignalR stateful reconnect in spec docs#66771

Open
BrennanConroy wants to merge 1 commit into
mainfrom
brennanconroy/stunning-chainsaw
Open

Document SignalR stateful reconnect in spec docs#66771
BrennanConroy wants to merge 1 commit into
mainfrom
brennanconroy/stunning-chainsaw

Conversation

@BrennanConroy
Copy link
Copy Markdown
Member

@BrennanConroy BrennanConroy commented May 21, 2026

Why

Issue #53103 points out that src/SignalR/docs/specs/TransportProtocols.md is out of date with respect to the stateful reconnect feature: it names the negotiate property useAck, but the shipped wire name is useStatefulReconnect, and it links to a literal [ack protocol](#todo) placeholder section that was never written. HubProtocol.md already had the wire formats for the Ack and Sequence messages, but no description of when/why they are exchanged or how sequence IDs and replay actually work.

What changed

TransportProtocols.md

  • Renamed the negotiate query-string / response property from useAck to useStatefulReconnect to match NegotiateProtocol.cs and HttpConnectionDispatcher.cs.
  • Replaced the dead [ack protocol](#todo) link with a real Stateful Reconnect section: opt-in handshake, WebSockets-only scope, and the reconnect flow (client reuses connectionToken on a new WebSocket, server reattaches instead of returning 409 Conflict, 404 if the connection has already been cleaned up).
  • Updated the WebSockets transport section to note that the "no second WebSocket" 409 Conflict rule is relaxed for stateful-reconnect reattach.
  • Scoped the transport's contract to reattach + notify the upper layer; explicitly defers buffering/ack/replay to upper layers, with the Hub protocol as a non-normative example.

HubProtocol.md

  • Added cross-references from the existing Ack / Sequence rows in the message-summary table to the new semantics section.
  • Added a Stateful Reconnect section that, after verifying against src/SignalR/common/Shared/MessageBuffer.cs, documents:
    • The trackable message set: HubInvocationMessage subtypes only (Invocation, StreamInvocation, StreamItem, Completion, CancelInvocation); Ping, Close, Ack, Sequence and handshakes are not tracked.
    • Per-direction 1-based sequence IDs that are not reset by a reconnect.
    • The duplicate-discard rule.
    • Periodic Ack semantics, including that a later Ack supersedes earlier ones for the same direction.
    • The Sequence-first reconnect handshake: sequenceId is highest_sent + 1, and the message is sent even when there are no buffered messages to replay so the peer knows where IDs continue.
  • Kept the section transport-agnostic; TransportProtocols.md is mentioned only as an example transport binding.

Fixes #53103

Notes for reviewers

  • Docs-only change. No code is modified.

Updates the SignalR spec docs to cover stateful reconnect (issue #53103).

TransportProtocols.md:

- Rename the negotiate parameter from useAck to useStatefulReconnect to match the shipped wire name.

- Replace the dead [ack protocol](#todo) placeholder link with a real Stateful Reconnect section: negotiation handshake, WebSockets-only scope, and reconnect flow (reuse connectionToken on a new WebSocket, server reattaches instead of returning 409 Conflict, 404 if the connection has already been cleaned up).

- Note in the WebSockets transport section that the 409 Conflict rule is relaxed for stateful-reconnect reattach.

- Scope the transport's responsibility to reattach + notify the upper layer; explicitly defer buffering/ack/replay to upper layers (Hub protocol as an example).

HubProtocol.md:

- Link the existing Ack/Sequence message-table rows to the new semantics section.

- Add a Stateful Reconnect section describing the trackable message set (HubInvocationMessage subtypes only), 1-based per-direction sequence IDs, duplicate-discard rule, periodic Ack semantics, and the Sequence-first reconnect handshake (carrying highest_sent + 1, sent even with no buffered messages).

- Keep the section transport-agnostic: TransportProtocols.md is referenced only as an example transport binding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 21, 2026 00:47
@BrennanConroy BrennanConroy requested a review from halter73 as a code owner May 21, 2026 00:47
@github-actions github-actions Bot added the area-signalr Includes: SignalR clients and servers label May 21, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the SignalR spec documentation to reflect the shipped “stateful reconnect” negotiate flag and to add explanatory text for the reconnect/ack/replay behavior (especially around Ack and Sequence messages) across transport and hub protocol docs.

Changes:

  • Renames the negotiate property from useAck to useStatefulReconnect and replaces the old placeholder link with a new Stateful Reconnect section in the transport spec.
  • Extends the WebSockets transport section to describe reconnect/reattach behavior when stateful reconnect is enabled.
  • Adds a Hub-protocol Stateful Reconnect section and cross-references from the Ack/Sequence message table entries.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/SignalR/docs/specs/TransportProtocols.md Updates negotiate flag naming and adds transport-level stateful reconnect documentation, including WebSockets reattach behavior.
src/SignalR/docs/specs/HubProtocol.md Adds hub-protocol semantics documentation for Ack/Sequence and stateful reconnect behavior.
Comments suppressed due to low confidence (2)

src/SignalR/docs/specs/TransportProtocols.md:218

  • In this section, the opening description states that “each side buffers … acknowledges … and replays” messages, but the last paragraph says the transport itself does not buffer/ack/replay and that this is defined by the upper layer. Consider rewording the opening paragraph to attribute buffering/ack/replay to the upper layer (e.g., Hub protocol) to avoid implying the transport layer provides these semantics.
Stateful reconnect lets a client transparently recover from a transport-level
disconnect without losing in-flight messages. When enabled, each side buffers
the messages it has sent and the receiver acknowledges the messages it has
processed; on reconnect the unacknowledged messages are replayed in order.

src/SignalR/docs/specs/HubProtocol.md:214

  • The duplicate-discard rule in this paragraph is incorrect/incomplete: after a reconnect, duplicates are identified relative to the receiver’s highest already-processed sequence ID (i.e., IDs <= highestProcessed are ignored). In practice, replayed messages can have IDs >= the Sequence.sequenceId but still be duplicates (e.g., replaying IDs that were already processed), so “strictly less than the new starting ID” is not the right condition.
The receiver uses the `sequenceId` from the incoming `Sequence` message to
align its inbound counter and discard duplicates: any subsequently
received trackable message with an ID strictly less than the new starting
ID is a duplicate from before the reconnect and must be ignored.

Comment on lines +150 to +152
When the client opts in to stateful reconnect (see [Stateful Reconnect](#stateful-reconnect)) it should also append the `useStatefulReconnect=true` query string parameter when opening the WebSocket. This signals that the request is a reconnect attempt for an existing connection and, on a fresh connection, that subsequent transport-level disconnects should be recoverable.

Establishing a second WebSocket connection when there is already a WebSocket connection associated with the Endpoints connection is not permitted and will fail with a `409 Conflict` status code. The one exception is when stateful reconnect was negotiated on the original connection: in that case, opening a new WebSocket to `[endpoint-base]` with the same `id` reattaches to the existing connection and the previous WebSocket is closed. If the existing connection has already been cleaned up (for example, the reconnect grace period has elapsed) the server responds with `404 Not Found` instead.
Comment on lines +206 to +214
whose `sequenceId` is the ID it will use for the **next** trackable
message it sends. This is equivalent to one greater than the highest
trackable ID it has previously sent, regardless of whether those messages
were acknowledged.

The receiver uses the `sequenceId` from the incoming `Sequence` message to
align its inbound counter and discard duplicates: any subsequently
received trackable message with an ID strictly less than the new starting
ID is a duplicate from before the reconnect and must be ignored.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-signalr Includes: SignalR clients and servers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update SignalR TransportProtocols doc

2 participants