Skip to content

Surprising behavior with IAsyncEnumerable: ConnectionLostException on disposing iterator #1174

@lostmsu

Description

@lostmsu

Repro

[Fact]
public async Task ConnectionLost_MoveNext()
{
    await foreach (int number in this.clientProxy.Value.GetNumbersAsync(this.TimeoutToken))
    {
    }

    // Simulate a connection loss by disposing the server.
    this.serverRpc.Dispose();
    await using var enumerator = this.clientProxy.Value.GetNumbersAsync(this.TimeoutToken)
                                                       .GetAsyncEnumerator(this.TimeoutToken);
    try
    {
        await enumerator.MoveNextAsync();
    } catch (ConnectionLostException e)
    {
        return;
    }
}

Expected

Test passes

Actual

Test fails with

  StreamJsonRpc.ConnectionLostException : The JSON-RPC connection with the remote party was lost before the request could complete.
  ---- System.OperationCanceledException : The operation was canceled.

Stack Trace: 
  JsonRpc.InvokeCoreAsync(JsonRpcRequest request, Type expectedResultType, CancellationToken cancellationToken) line 1982
  JsonRpc.InvokeCoreAsync[TResult](RequestId id, String targetName, IReadOnlyList`1 arguments, IReadOnlyList`1 positionalArgumentDeclaredTypes, IReadOnlyDictionary`2 namedArgumentDeclaredTypes, CancellationToken cancellationToken, Boolean isParameterObject) line 1564
  <<-ctor>b__0>d.MoveNext() line 1076
  --- End of stack trace from previous location ---
  ExecuteContinuationSynchronouslyAwaiter`1.GetResult()
  <<GetValueAsync>b__0>d.MoveNext()
  --- End of stack trace from previous location ---
  AsyncEnumeratorProxy.DisposeAsync() line 1102
  AsyncEnumerableTests.ConnectionLost_MoveNext() line 469
  --- End of stack trace from previous location ---
  ----- Inner Stack Trace -----
  CancellationToken.ThrowOperationCanceledException()
  CancellationToken.ThrowIfCancellationRequested()
  MessageHandlerBase.WriteAsync(JsonRpcMessage content, CancellationToken cancellationToken) line 218
  JsonRpc.SendAsync(JsonRpcMessage message, CancellationToken cancellationToken) line 1720
  JsonRpc.InvokeCoreAsync(JsonRpcRequest request, Type expectedResultType, CancellationToken cancellationToken) line 1949

Remarks

This happens because DisposeAsync throws when connection is already gone.

It breaks ergonomics on using async iterators as you can no longer rely on await using var iterator = ...GetAsyncEnumerator to correctly dispose the iterator on failure - you have to wrap the whole thing in try ... catch. Normally you only have to do it for MoveNextAsync.

It also violates CA1065

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions