Search code examples
gogrpcgrpc-go

is a nil server message possible with gRPC?


In the following gRPC-client code, is the second if necessary?

status, err := cli.GetStatus(ctx, &empty.Empty{})
if err != nil {
    return err
}

if status == nil {
    // this should NEVER happen - right?
    return fmt.Errorf("nil Status result returned") 
}

Intuitively, one should always check for nil in go just in case. However, there is a runtime check to catch any client-to-server nil usage e.g.

status, err := cli.GetStatus(ctx, nil) // <- runtime error

if err != nil {
    // "rpc error: code = Internal desc = grpc: error while marshaling: proto: Marshal called with nil"
    return err
}

So is there a similar server-to-client runtime guarantee, and thus remove the need for a status == nil check?


Solution

  • Investigating further with a contrived server example:

    func (s *mygRPC) GetStatus(context.Context, *empty.Empty) (*pb.Status, error) {
        log.Println("cli: GetStatus()")
    
        //return &pb.Status{}, nil
        return nil, nil // <- can server return a nil status message (with nil error)
    }
    

    and testing client/server reactions:

    CLIENT:

    ERROR: rpc error: code = Internal desc = grpc: error while marshaling: proto: Marshal called with nil
    

    SERVER:

    2019/05/14 16:09:50 cli: GetStatus()
    ERROR: 2019/05/14 16:09:50 grpc: server failed to encode response:  rpc error: code = Internal desc = grpc: error while marshaling: proto: Marshal called with nil
    

    So even if one wanted to legitimately return a nil value, the gRPC transport will not allow it.

    Note: the server-side code is still executed - as expected - but as far as the client is concerned, the gRPC call failed.

    Conclusion: a valid (err==nil) server response will always return a valid (non-nil) message.


    EDIT:

    Inspecting the gRPC source reveals where a nil message is caught:

    server.go

    func (s *Server) sendResponse(t transport.ServerTransport, stream *transport.Stream, msg interface{}, cp Compressor, opts *transport.Options, comp encoding.Compressor) error {
        data, err := encode(s.getCodec(stream.ContentSubtype()), msg)
        if err != nil {
            grpclog.Errorln("grpc: server failed to encode response: ", err)
            return err
        }
        // ...
    }
    

    rpc_util.go

    func encode(c baseCodec, msg interface{}) ([]byte, error) {
        if msg == nil { // NOTE: typed nils will not be caught by this check
            return nil, nil
        }
        b, err := c.Marshal(msg)
        if err != nil {
            return nil, status.Errorf(codes.Internal, "grpc: error while marshaling: %v", err.Error())
        }
        // ...
    }
    

    The comment in this line is key:

    if msg == nil { // NOTE: typed nils will not be caught by this check }
    

    So if one were to use reflect on our typed-nil, reflect.ValueOf(msg).IsNil() would return true. The following c.Marshal(msg) errors - and the call fails to send a message response to the client.