SignalR: keep-alive ping and stateful reconnect ack race on Transport.Output, corrupting the stream
I maintainer di solito rispondono entro 1 giorno
@MaratheSS ci sta già lavorando.
Dal 5/11/2026.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 38/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- csharp
- Ambito
- backend, networking
Direzione di ricerca
Start with HubConnectionContext.cs and MessageBuffer.cs on the release/10.0 branch, tracing the ping writers and MessageBuffer.RunTimer to understand how they write to Transport.Output. Then inspect the related tests and reproduce the overlap between a keep-alive ping and a stateful reconnect acknowledgment. Done means concurrent writes cannot corrupt the stream and a regression test covers the race.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Is there an existing issue for this?
I searched the issues and found nothing matching. PR #65732 touches MessageBuffer write cancellation but not this race.
Describe the bug
With stateful reconnect on, the server can write a keep-alive ping and a stateful reconnect AckMessage to the same PipeWriter at the same time. Each writer holds a different lock, so nothing serializes them. The bytes mix on the wire, and the client fails to parse the stream and drops the connection.
The two writers in release/10.0:
Normal hub messages are safe: HubConnectionContext.WriteCore calls _messageBuffer.WriteAsync while holding HubConnectionContext._writeLock, and MessageBuffer then takes its own lock. But the ping path never takes MessageBuffer._writeLock, and the ack timer never takes HubConnectionContext._writeLock. The ping path doesn't check UsingStatefulReconnect() either, so it bypasses the buffer.
The window opens when the server has sent nothing for KeepAliveInterval, so a ping is due, while the client keeps sending messages that need an ack, for example the reply to a long-running server-to-client request.
Expected behavior
Pings and acks are serialized with each other and with normal hub messages, so the stream stays well formed.
Steps to reproduce
We have no minimal repro yet. Observed setup:
- ASP.NET Core server hub with
AllowStatefulReconnects = true,KeepAliveInterval = 8 s,ClientTimeoutInterval = 17.6 s, JSON protocol. - .NET client (
Microsoft.AspNetCore.SignalR.Client10.0.13) with.WithStatefulReconnect(),.WithAutomaticReconnect(...),WithKeepAliveInterval(8 s),WithServerTimeout(17.6 s), WebSockets transport over localhost. - The server calls client methods through
IHubContext<T>.Clients.All.SendCoreAsync, and the client replies by invoking a hub method. - The connection drops 10 to 15 times a day at irregular times. We are shipping a build with stateful reconnect off and will report whether the drops stop.
A repro would likely need a server that sends nothing while the client invokes hub methods often, with a short KeepAliveInterval to make collisions more frequent.
Exceptions (if any)
Client side, raised as HubConnection.Reconnecting. The first character changes from drop to drop: 's', 'o', and an empty message.
System.IO.InvalidDataException: Error reading JSON.
---> System.Text.Json.JsonReaderException: 's' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.
at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence`1 input, IInvocationBinder binder)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence`1 input, IInvocationBinder binder)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.ReceiveLoop(ConnectionState connectionState)
System.IO.InvalidDataException: Error reading JSON.
---> System.Text.Json.JsonReaderException: The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. LineNumber: 0 | BytePositionInLine: 0.
at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence`1 input, IInvocationBinder binder)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence`1 input, IInvocationBinder binder)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.ReceiveLoop(ConnectionState connectionState)
The server logs only a normal disconnect, with no exception.
.NET Version
- Server: Microsoft.AspNetCore.App 10.0.14, Windows, Kestrel.
- Client: Microsoft.AspNetCore.SignalR.Client 10.0.13, net10.0.
Anything else?
Possible fixes: have the ack timer take HubConnectionContext._writeLock, for example by passing that lock into MessageBuffer. Or, when stateful reconnect is on, route pings through MessageBuffer under its lock.
We haven't checked whether the .NET client has the same ping and ack pattern in the other direction.
Workaround: turn off stateful reconnect.
Is there an existing issue for this? I searched the issues and found nothing matching. PR #65732 touches MessageBuffer write cancellation but not this race.Describe the bug
With stateful reconnect on, the server can write a keep-alive ping and a stateful reconnect AckMessage to the same PipeWriter at the same time. Each writer holds a different lock, so nothing serializes them. The bytes mix on the wire, and the client fails to parse the stream and drops the connection.
The two writers in release/10.0:
Writer Lock held Writes to
HubConnectionContext.TryWritePingAsync / TryWritePingSlowAsync (HubConnectionContext.cs) HubConnectionContext._writeLock _connectionContext.Transport.Output
MessageBuffer.RunTimer (MessageBuffer.cs), every AckRate (1 s) when _lastAckedId < _latestReceivedSequenceId MessageBuffer._writeLock _writer, set from connection.Transport.Output in the constructor
Normal hub messages are safe: HubConnectionContext.WriteCore calls _messageBuffer.WriteAsync while holding HubConnectionContext._writeLock, and MessageBuffer then takes its own lock. But the ping path never takes MessageBuffer._writeLock, and the ack timer never takes HubConnectionContext._writeLock. The ping path doesn't check UsingStatefulReconnect() either, so it bypasses the buffer.
The window opens when the server has sent nothing for KeepAliveInterval, so a ping is due, while the client keeps sending messages that need an ack, for example the reply to a long-running server-to-client request.
Expected behavior
Pings and acks are serialized with each other and with normal hub messages, so the stream stays well formed.
Steps to reproduce
We have no minimal repro yet. Observed setup:
ASP.NET Core server hub with AllowStatefulReconnects = true, KeepAliveInterval = 8 s, ClientTimeoutInterval = 17.6 s, JSON protocol.
.NET client (Microsoft.AspNetCore.SignalR.Client 10.0.13) with .WithStatefulReconnect(), .WithAutomaticReconnect(...), WithKeepAliveInterval(8 s), WithServerTimeout(17.6 s), WebSockets transport over localhost.
The server calls client methods through IHubContext.Clients.All.SendCoreAsync, and the client replies by invoking a hub method.
The connection drops 10 to 15 times a day at irregular times. We are shipping a build with stateful reconnect off and will report whether the drops stop.
A repro would likely need a server that sends nothing while the client invokes hub methods often, with a short KeepAliveInterval to make collisions more frequent.
Exceptions (if any)
Client side, raised as HubConnection.Reconnecting. The first character changes from drop to drop: 's', 'o', and an empty message.
System.IO.InvalidDataException: Error reading JSON.
---> System.Text.Json.JsonReaderException: 's' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.
at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan1 bytes) at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker) at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first) at System.Text.Json.Utf8JsonReader.ReadSingleSegment() at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence1 input, IInvocationBinder binder)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence1 input, IInvocationBinder binder) at Microsoft.AspNetCore.SignalR.Client.HubConnection.ReceiveLoop(ConnectionState connectionState) System.IO.InvalidDataException: Error reading JSON. ---> System.Text.Json.JsonReaderException: The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. LineNumber: 0 | BytePositionInLine: 0. at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan1 bytes)
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence1 input, IInvocationBinder binder) --- End of inner exception stack trace --- at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.ParseMessage(ReadOnlySequence1 input, IInvocationBinder binder)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.ReceiveLoop(ConnectionState connectionState)
The server logs only a normal disconnect, with no exception.
.NET Version
Server: Microsoft.AspNetCore.App 10.0.14, Windows, Kestrel.
Client: Microsoft.AspNetCore.SignalR.Client 10.0.13, net10.0.
Anything else?
Possible fixes: have the ack timer take HubConnectionContext._writeLock, for example by passing that lock into MessageBuffer. Or, when stateful reconnect is on, route pings through MessageBuffer under its lock.
We haven't checked whether the .NET client has the same ping and ack pattern in the other direction.
Workaround: turn off stateful reconnect.
- Lingua principale
- C#
- Stelle
- 38.5k
- Fork
- 11.6k
- Merge medio
- 3g 1h
- PR unite (30g)
- 289
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di dotnet/aspnetcore
-
area-networking
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/aspnetcore#70545 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
area-infrastructure
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
dotnet/aspnetcore#70524 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
[OpenTelemetry] Hosting replaces an Activity.DisplayName that the app set, when a route matchesApertaarea-hosting Regression
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/aspnetcore#70516 · 1 commento · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
[SSE] ServerSentEventsResult.WrapEvents is missing [EnumeratorCancellation], so RequestAborted never reaches the app's generator on the IAsyncEnumerable<T> overloadForse già presa @bharatprajapati-coder l’ha presa 1 giorno fa. Apertaarea-minimal
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/aspnetcore#70419 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
[Blazor] DefaultInMemoryCircuitPersistenceProvider never disposes the retention CancellationTokenSource when a persist is rejected at capacityForse già presa @Vinoth2562000 l’ha presa 1 giorno fa. Apertaarea-blazor
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
dotnet/aspnetcore#70418 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di dotnet/aspnetcore
Issue simili
-
bug emitter:client:csharp
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
microsoft/typespec#12572 · 1 commento · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
needs-area-label
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 2 giorni
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledApertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
newrelic/newrelic-dotnet-agent#3949 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 87/100
I maintainer di solito rispondono entro 1 giorno
-
priority:P2
Difficoltà 2/5 1-3 ore Idoneità per principianti 80/100