What version of gRPC and what language are you using?
Grpc.Net.Client 2.76.0
What operating system (Linux, Windows,...) and version?
Windows 10
What runtime / compiler are you using (e.g. .NET Core SDK version dotnet --info)
.net 10
What did you do?
If possible, provide a recipe for reproducing the error. Try being specific and include code snippets if helpful.
Read things from server:
try
{
while (await call.ResponseStream.MoveNext().ConfigureAwait(false))
{
// stuff
}
}
catch (Exception e)
{
// log
}
Some time later, I came back and I was met by unhandled excpetion.
What did you expect to see?
No unhandled exception:-)
What did you see instead?
It is strange, as I am catching all. Also the unhandled exception stack trace is cut short and start out of nowhere.
.NETCoreApp,Version=v10.0
System.Net.Http.HttpProtocolException: The HTTP/2 server closed the connection. HTTP/2 error code 'STREAM_CLOSED' (0x5). (HttpProtocolError)
at System.Net.Http.Http2Connection.ThrowRequestAborted(Exception innerException)
at System.Net.Http.Http2Connection.Http2Stream.TryReadFromBuffer(Span`1 buffer, Boolean partOfSyncRead)
at System.Net.Http.Http2Connection.Http2Stream.ReadDataAsync(Memory`1 buffer, HttpResponseMessage responseMessage, CancellationToken cancellationToken)
at Grpc.Net.Client.Internal.StreamExtensions.ReadMessageAsync[TResponse](Stream responseStream, GrpcCall call, Func`2 deserializer, String grpcEncoding, Boolean singleMessage, CancellationToken cancellationToken)
at Grpc.Net.Client.Internal.GrpcCall`2.ReadMessageAsync(Stream responseStream, String grpcEncoding, Boolean singleMessage, CancellationToken cancellationToken)
at Grpc.Net.Client.Internal.HttpContentClientStreamReader`2.MoveNextCore(CancellationToken cancellationToken)
Looking at the code, I find it a bit supicious:
E:\git\grpc-dotnet\src\Grpc.Net.Client\Internal\HttpContentClientStreamReader.cs
Line 112:
// Save move next task to track whether it is complete
_moveNextTask = MoveNextCore(cancellationToken);
Anything else we should know about your project / environment?
I asked Claude:
Yes, there's something interesting here. Look at this pattern carefully:
lock (_moveNextLock)
{
_moveNextTask = MoveNextCore(cancellationToken); // assigned INSIDE lock
}
return _moveNextTask; // read OUTSIDE lock
_moveNextTask is assigned inside the lock, but the field is read again at return _moveNextTask outside the lock. These are two separate field accesses. If anything modifies _moveNextTask between the lock being released and the return, the caller gets back a different task than the one that was just created — and the original faulted task has no reference, becomes unobserved, and its exception escapes via the unobserved task machinery.
Now look at the catch block inside MoveNextCore:
catch (Exception ex)
{
var newException = _call.ResolveException("Error reading next message.", ex, out var status, out var resolvedException);
if (!_call.ResponseFinished)
{
_call.ResponseStreamEnded(status.Value, finishedGracefully: false); // ? this
}
...
throw;
}
_call.ResponseStreamEnded completes CallTask. That completion could trigger continuations. And look at the very beginning of MoveNext():
if (_call.CallTask.IsCompletedSuccessfully())
{
...
return Task.FromException<bool>(_call.CreateRpcException(status));
}
If a continuation triggered by ResponseStreamEnded causes MoveNext() to be called again — even indirectly — it would acquire the lock, set _moveNextTask to a NEW task, and release the lock. Then the original caller's return _moveNextTask reads the NEW task, returns it, and the ORIGINAL faulted task with the raw HttpProtocolException has no observer.
This would perfectly explain:
- Raw
HttpProtocolException escaping unwrapped
- Zero user frames — the task was never awaited by user code
- It being a one-off race condition
What version of gRPC and what language are you using?
Grpc.Net.Client 2.76.0
What operating system (Linux, Windows,...) and version?
Windows 10
What runtime / compiler are you using (e.g. .NET Core SDK version
dotnet --info).net 10
What did you do?
If possible, provide a recipe for reproducing the error. Try being specific and include code snippets if helpful.
Read things from server:
Some time later, I came back and I was met by unhandled excpetion.
What did you expect to see?
No unhandled exception:-)
What did you see instead?
It is strange, as I am catching all. Also the unhandled exception stack trace is cut short and start out of nowhere.
Looking at the code, I find it a bit supicious:
E:\git\grpc-dotnet\src\Grpc.Net.Client\Internal\HttpContentClientStreamReader.csLine 112:
Anything else we should know about your project / environment?
I asked Claude:
Yes, there's something interesting here. Look at this pattern carefully:
_moveNextTaskis assigned inside the lock, but the field is read again atreturn _moveNextTaskoutside the lock. These are two separate field accesses. If anything modifies_moveNextTaskbetween the lock being released and thereturn, the caller gets back a different task than the one that was just created — and the original faulted task has no reference, becomes unobserved, and its exception escapes via the unobserved task machinery.Now look at the
catchblock insideMoveNextCore:_call.ResponseStreamEndedcompletesCallTask. That completion could trigger continuations. And look at the very beginning ofMoveNext():If a continuation triggered by
ResponseStreamEndedcausesMoveNext()to be called again — even indirectly — it would acquire the lock, set_moveNextTaskto a NEW task, and release the lock. Then the original caller'sreturn _moveNextTaskreads the NEW task, returns it, and the ORIGINAL faulted task with the rawHttpProtocolExceptionhas no observer.This would perfectly explain:
HttpProtocolExceptionescaping unwrapped