gRPC

1. Status of server-side response

Developers doing back-end services are always sensitive to error handling, so they will always be very careful when doing the service response (response/reply) design.

If the back-end service is selected from HTTP API (rest api), such as json over http, the API response (Response) will mostly contain the following information.

1
2
3
4
5
6
7
{
    "code": 0,
    "msg": "ok",
    "payload" : {
        ... ...
    }
}

In the response design of this http api, the first two states identify the response status of the request. This status consists of a status code (code) and a status message (msg). The status message is a detailed explanation of the cause of the error corresponding to the status code. The payload follows only when the status is normal (code = 0), and the payload is obviously the business information intended to be passed to the client in the response.

Such a service response design is currently more common and mature program, it is also very easy to understand.

Okay, now let’s look at another big class of services: services provided by RPC. Let’s take the most widely used gRPC as an example. In gRPC, a service is defined as follows (let’s borrow the helloworld example provided by grpc-go).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto
package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

grpc has a constraint for each rpc method (e.g. SayHello) that it can only have one input parameter and one return value. The go code generated by this .proto definition via protoc becomes this.

1
2
3
4
5
6
// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld_grpc.pb.go
type GreeterServer interface {
    // Sends a greeting
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
    ... ...
}

We see that for the SayHello RPC method, the go code generated by protoc has an additional error return value in the SayHello method’s return value list that Gopher’s are familiar with. For gophers who are used to the HTTP API response design, here’s the problem! Is the code and msg in the http api response that represent the status of the response defined in the HelloReply business response data, or is it returned via error? This grpc official documentation does not seem to specify (if you find the location, you can tell me oh).

2. gRPC server-side response design ideas

We are not in a hurry to draw conclusions! We continue to borrow helloworld this sample program to test the response of the client when the error return value is not nil! First, change the code of greeter_server.

1
2
3
4
5
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return &pb.HelloReply{Message: "Hello " + in.GetName()}, errors.New("test grpc error")
}

In the above code, we deliberately construct an error and return it to the client that called the method. Let’s run the service and start greeter_client to access the service. On the client side, we get the following result are as follows.

1
2021/09/20 17:04:35 could not greet: rpc error: code = Unknown desc = test grpc error

From the output of the client, we see the content of our custom error (test grpc error). But we also find that the error output also has a “code = Unknown” output. It seems that grpc expects the error to be in the form of code and desc.

At this point, I have to check the gprc-go (v1.40.0) reference documentation! In the documentation of grpc-go we find several functions related to Error that are DEPRECATED.

gprc-go (v1.40.0) reference documentation

The documentation for these deprecated functions mentions replacing them with functions of the same name from the status package. So who is this status package? Looking through the source code of grpc-go, we finally found the status package, and we found the answer in the first sentence of the package description.

1
Package status implements errors returned by gRPC.

It turns out that the status package implements the type of error expected by the grpc client above. So what does this type look like? Let’s trace the code step by step.

In the grpc-go/status package we see the following code.

1
2
3
4
5
6
type Status = status.Status

// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
    return status.New(c, msg)
}

The status package uses Status from the internal/status package. Let’s look again at the definition of the Status structure in the internal/status package.

1
2
3
4
5
6
7
8
9
// internal/status
type Status struct {
    s *spb.Status
}

// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
    return &Status{s: &spb.Status{Code: int32(c), Message: msg}}
}

The Status structure of the internal/status package combines a field of type *spb.Status (the type in the google.golang.org/genproto/googleapis/rpc/statu s package). Continue tracing the spb.Status.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status
type Status struct {
    // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
    Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
    // A developer-facing error message, which should be in English. Any
    // user-facing error message should be localized and sent in the
    // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
    Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
    // A list of messages that carry the error details.  There is a common set of
    // message types for APIs to use.
    Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"`
    // contains filtered or unexported fields
}

We see that this last Status structure contains Code and Message, so it is clear that grpc is designed to expect the developer to include the response status of the rpc method in the return value of error, while the custom response structure only needs to contain the data required by the business. Let’s use a diagram to establish the mapping between the http api and the rpc response horizontally.

http api and the rpc response horizontally.

With this diagram in hand, we’ll be well-equipped to face the problem of how to design grpc method responses!

grpc-go defines more than 10 error codes required by the grpc specification in the codes package.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const (
    // OK is returned on success.
    OK Code = 0

    // Canceled indicates the operation was canceled (typically by the caller).
    //
    // The gRPC framework will generate this error code when cancellation
    // is requested.
    Canceled Code = 1

    // Unknown error. An example of where this error may be returned is
    // if a Status value received from another address space belongs to
    // an error-space that is not known in this address space. Also
    // errors raised by APIs that do not return enough error information
    // may be converted to this error.
    //
    // The gRPC framework will generate this error code in the above two
    // mentioned cases.
    Unknown Code = 2

    // InvalidArgument indicates client specified an invalid argument.
    // Note that this differs from FailedPrecondition. It indicates arguments
    // that are problematic regardless of the state of the system
    // (e.g., a malformed file name).
    //
    // This error code will not be generated by the gRPC framework.
    InvalidArgument Code = 3

    ... ...

    // Unauthenticated indicates the request does not have valid
    // authentication credentials for the operation.
    //
    // The gRPC framework will generate this error code when the
    // authentication metadata is invalid or a Credentials callback fails,
    // but also expect authentication middleware to generate it.
    Unauthenticated Code = 16

In addition to these standard error codes, we can also extend to define our own error codes and error descriptions.

3. How the server side constructs error and how the client side parses error

As mentioned earlier, the gRPC server uses the last return value of the rpc method, error, to carry the response status. The google.golang.org/grpc/status package provides some convenient functions for constructing client-side parsable error, let’s look at the following example (based on the above helloworld’s greeter_server) blob/master/examples/helloworld/greeter_server/main.go) is modified).

1
2
3
4
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return nil, status.Errorf(codes.InvalidArgument, "you have a wrong name: %s", in.GetName())
}

The status package provides a function similar to fmt.Errorf, so we can easily construct an error instance with code and msg and return it to the client.

The client can also parse out the information carried in the error through the functions provided by the status package, as shown in the following code.

1
2
3
4
5
6
7
ctx, _ := context.WithTimeout(context.Background(), time.Second)
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "tony")})
if err != nil {
    errStatus := status.Convert(err)
    log.Printf("SayHello return error: code: %d, msg: %s\n", errStatus.Code(), errStatus.Message())
}
log.Printf("Greeting: %s", r.GetMessage())

We see that: the information carried in the error returned by the rpc method that is not nil can be extracted very briefly by the status.Convert function.

4. Empty response

The gRPC proto file specification requires that the definition of each rpc method must contain a return value, and the return value cannot be null, such as the SayHello method in the .proto file of the above helloworld project.

1
rpc SayHello (HelloRequest) returns (HelloReply) {}

If you remove the HelloReply return value, then protoc will report an error when generating code!

But some methods do not need to return business data themselves, so we need to define an empty response message for them, e.g.

1
2
3
message Empty {

}

Considering that every project has to repeat the wheel defined by Empty message above when it encounters an empty response, grpc officially provides an empty message that can be reused.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/empty.proto

// A generic empty message that you can re-use to avoid defining duplicated
// empty messages in your APIs. A typical example is to use it as the request
// or the response type of an API method. For instance:
//
//     service Foo {
//       rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
//     }
//
// The JSON representation for `Empty` is empty JSON object `{}`.
message Empty {}

We just need to import that empty.proto in the .proto file and use Empty, like the following code.

1
2
3
4
5
6
7
8
9
// xxx.proto

syntax = "proto3";

import "google/protobuf/empty.proto";

service MyService {
    rpc MyRPCMethod(...) returns (google.protobuf.Empty);
}

Of course google.protobuf.Empty is not just for empty responses, but also for empty requests, which is left to you to complete on your own.

5. Summary

In this article, we talked about gRPC server-side response design, the main point is to directly use the gRPC-generated rpc aspect of the error return value to indicate the response status of the rpc call, and not to repeat the code and msg fields in the custom Message structure to indicate the response status.

btw, to do the API error design, google this API design reference is very good. You must have time to read it well.