colm.anseo
colm.anseo

Reputation: 22097

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?

Upvotes: 6

Views: 6402

Answers (2)

colm.anseo
colm.anseo

Reputation: 22097

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.

Upvotes: 8

Vitaly Isaev
Vitaly Isaev

Reputation: 5815

Yes, this should never happen. GRPC is responsible for that.

Upvotes: 2

Related Questions