[Ruby] Tips on error handling of gRPC in Ruby

4 minute read

Summarize error handling tips when implementing a gRPC client in Ruby

Premise

  • Ruby 2.3.8
  • Rails 5.0.7.2
  • grpc gem 1.2.8

tl;dr

  • Since the error response to the gRPC call is an exception of type GRPC::BadStatus, be sure to resuce when making a gRPC call.
  • If you want to get the detailed information of the caught Exception, cast it to Google::Rpc::Status type with the to_rpc_status method.
  • If the gRPC server returns error details, it will be returned as an Any type, so you need to unpack it to the appropriate type.
  • gem error class https://github.com/grpc/grpc/blob/master/src/ruby/lib/grpc/errors.rb

First catch the error

The response of gRPC call is basically one of the following Codes.

enum Code {
  OK = 0;
  CANCELLED = 1;
  UNKNOWN = 2;
  INVALID_ARGUMENT = 3;
  DEADLINE_EXCEEDED = 4;
  NOT_FOUND = 5;
  ALREADY_EXISTS = 6;
  PERMISSION_DENIED = 7;
  UNAUTHENTICATED = 16;
  RESOURCE_EXHAUSTED = 8;
  FAILED_PRECONDITION = 9;
  ABORTED = 10;
  OUT_OF_RANGE = 11;
  UNIMPLEMENTED = 12;
  INTERNAL = 13;
  UNAVAILABLE = 14;
  DATA_LOSS = 15;
}

https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto As an implementation of gRPC server, I think that it will be made to return Code OK(0) for normal system and Code(1~15) other than OK in case of error.

When an error is returned, it seems that the exception is raised on the grpc gem side, and if it is not rescued, the program will stop there. So you need to pick up the exception with rescue as follows:

    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.get_hoge
    rescue GRPC::BadStatus => ex
      res = "error"
    rescue ex
      res = "unexpected error"
    end

Looking at the error class defined in the gem, all classes are BadStatus You can see that it inherits class.

In fact, if you look at the inheritance tree of the Exception that occurred

p ex.class.ancestors
# =>[GRPC::InvalidArgument, GRPC::BadStatus, GRPC::Core::StatusCodes, StandardError, Exception...

Therefore, basically all Exceptions can be caught with GRPC::BadStatus.

If you want to process it in more detail, you can pick it up by a unit such as GRPC::InvalidArgument.

Extract information from the captured Exception

Let’s extract the information from the captured Exception. The BadStatus instance has four fields. https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L49-L52

For example, create an error from the gRPC server implemented in Go and return it to ruby as follows.

server.go


  func (s *Server) set_name(name string) {
    st := status.New(codes.InvalidArgument, "invalid username")
    return nil, st.Err()
  }

client.rb


    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.set_name("hoge1")
    rescue GRPC::BadStatus => ex
      p ex.code # => 3
      p ex.message # => "3:invalid username"
      p ex.details # => "invalid username"
      res = "error"
    rescue ex
      res = "unexpected error"
    end

Status Code 3 and message ““invalid username”` were obtained

If you want to get the # error details, use the to_rpc_status method Up to the above, Status Code and error message could be obtained. However, when you actually build it, you will often use error details to pass the error details. https://christina04.hatenablog.com/entry/grpc-error-details https://grpc.io/docs/guides/error/#richer-error-model

If you want detailed information including error details, you can get more detailed information by using the to_rpc_status method. The implementation of to_rpc_status is as follows, but by using this, it can be cast to the Google::Rpc::Status type and detailed information including trailer metadata can be retrieved. https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L63-L75

I added error details to the previous implementation example.

server.go


  func (s *Server) set_name(name string) {
    st := status.New(codes.InvalidArgument, "invalid username")

    // Create and set error details information
    desc := "The username must only contain alphanumeric characters"
    v := &errdetails.BadRequest_FieldViolation{
Field: "username",
Description: desc,
    }
    br := &errdetails.BadRequest{}
    br.FieldViolations = append(br.FieldViolations, v)
    st, _ = st.WithDetails(br)
    
    return nil, st.Err()
  }

client.rb


require'google/rpc/error_details_pb'

    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.set_name("hoge1")
    rescue GRPC::BadStatus => ex
      p ex.class # => GRPC::InvalidArgument
      p ex.to_rpc_status.class # => Google::Rpc::Status
      p ex.to_rpc_status # => <Google::Rpc::Status: code: 3, message: "invalid username", details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google. rpc.BadRequest", value: "\nC\n\tusername\x126The username must only contain alphanumeric characters">]>

      ex.to_rpc_status.details.each do |detail|
        p detail.type_url # => "type.googleapis.com/google.rpc.BadRequest"
        p detail.unpack(Google::Rpc::BadRequest) # => <Google::Rpc::BadRequest: field_violations: [<Google::Rpc::BadRequest::FieldViolation: field: "username", description: "The username must only contain alphanumeric characters">]>
      end
      res = "error"
    rescue ex
      res = "unexpected error"
    end

Note that details details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google.rpc.BadRequest", value: "\nC\n\tusername2\x126The username must only contain alphanumeric The character ">] part.This is the error details data sent as a trailer, and there are some Google::Protobuf::Any type data and value data. You can set multiple error details and they will be returned as an array, so you need to rotate each to retrieve it. type_url stores the location where the type of error details is defined, and if the type provided by Google is used by default, it becomes "type.googleapis.com/google.rpc.BadRequest". .. Of course you can also set your own type as error details, in which case the location you defined in your proto file will be stored.

Also, the returned instance is of type Google::Protobuf::Any, and if it is left as it is, it cannot be retrieved properly with the data serialized. Therefore, the unpack method is used. By using type_url to determine the type and using unpack to cast it to the desired type, I was able to finally retrieve the error details. By the way, note that it is necessary to import google/rpc/error_details_pb in order to refer to the type definition of Google::Rpc::BadRequest.

Remarks

It took a lot of time to retrieve the error details. The sample code at https://grpc.io/docs/languages/ruby/quickstart/ doesn’t have this much detail, so it’s a bit painful that you have to go to the contents of the gem after all.