Search code examples
c#.netgrpcgrpc-dotnet

Is there a way to determine on a client that server prematurely completed client-streaming call by returning a response?


I have the following case. A client uploads files to a server using client-streaming (via grpc-dotnet). In some cases, a server might decide to ignore the client stream and return a response immediately. Since the client is unaware of the server's decision it continues to call RequestStream.WriteAsync() and eventually gets Grpc.Core.RpcException: 'Status(StatusCode="OK", Detail="")'

The question is: is there a graceful way to know in advance that the call is already completed without making faulty write attempts?

Skimming through the grpc-dotnet source code I managed to find a solution via a reflection AsyncClientStreamingCall.RequestStream.Call.ResponseFinished flag but it looks even worse than handling exception with a known StatusCode.

Client pseudo code:

private static async Task CallUploadStream(GrpcService.GrpcServiceClient client)
{
    using var streamingCall = client.UploadStream();

    // First call
    await WriteAsync();

    // Delay
    await Task.Delay(TimeSpan.FromSeconds(1));

    // Second call
    await WriteAsync(); // Grpc.Core.RpcException: 'Status(StatusCode="OK", Detail="")'

    await streamingCall.RequestStream.CompleteAsync();
    await streamingCall;

    return;

    Task WriteAsync()
    {
        return streamingCall.RequestStream.WriteAsync(
            new UploadStreamRequest
            {
                Bytes = ByteString.CopyFrom(new byte[1]),
            }
        );
    }
}

Protobuf contract:

syntax = "proto3";
package grpc.debug.contract.v1;
option csharp_namespace = "GrpcDebug.Contract";
import "google/protobuf/empty.proto";

service GrpcService {
  rpc UploadStream(stream UploadStreamRequest) returns (google.protobuf.Empty);
}

message UploadStreamRequest { bytes bytes = 1; }

The server that immediately returns response:

public class GrpcServiceV1 : GrpcService.GrpcServiceBase
{
    public override Task<Empty> UploadStream(IAsyncStreamReader<UploadStreamRequest> requestStream,
        ServerCallContext context)
    {
        return Task.FromResult(new Empty());
    }
}

Solution

  • So to be clear: is it the case that the call to WriteAsync faults with RpcException? You could try checking call.ResponseAsync.IsCompleted before each write (or perhaps each bit of work to get the next chunk), however this is inherently a race condition and you must always be ready to catch the exception even if you check on the line before. If what you're emulating here is basically a Stream, or at least: you're sending a large payload in multiple chunks, then you may also be interested in the protobuf-net.Grpc work to add binary streaming support natively; in the version on NuGet this is currently limited to servers returning a Stream, but that's mostly because that's the scenario that I had a consumer needing urgently - the plan is to add all the direction combinations, and support for APIs other than Stream, such as Pipe, etc.