Let's handle proto errors correctly :)

In programming, there are always several ways to solve the same problem. But not all of them are equally effective. Today we will talk about error handling methods in gRPC – good and bad.

message Result {
  oneof response {
    error.v1.Error error = 1;
    info.v1.Info info = 2;
  }
}

At first glance it may seem that using oneof to represent either the error or the result is convenient. However, this approach introduces unnecessary complexity into the messaging protocol and makes the code less readable. gRPC provides built-in error handling that allows you to communicate error information elegantly and efficiently.

Why use it? oneof for errors is a bad idea? First, it makes it difficult to use the standard gRPC error mechanism and status codes that are intended for this purpose. Second, it can lead to confusion on the client side when it comes to distinguishing successful responses from errors.

The diagram shows how processing two types of requests adds weight to the client's logic.

The diagram shows how processing two types of requests adds weight to the client's logic.

gRPC Error Codes

Error codes in the gRPC architecture are especially important for effective communication between the client and the server. They help the client understand the cause of the problem and respond to it correctly.

Proper and effective error handling in gRPC is key to building robust and maintainable systems. Using standard gRPC error codes and mechanisms not only simplifies client-side error handling, but also ensures that system behavior is clear and predictable. Instead of using constructs like oneof For error handling, it is better to use gRPC's built-in capabilities to transmit detailed error information.

Here's how you can use gRPC's codes.NotFound to signal that something is missing

import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"

// ...

err := status.Error(codes.NotFound, "не нашел котика")

// ...

This approach simplifies error handling on the client side, making it easier to understand the structure of the response data. In addition, errors returned via status.Error are converted to HTTP statuses when transported via gRPC-Gateway, in which case the errors become understandable outside of gRPC.

But what if we need more flexibility in the error response? For example, add additional meta-info or custom error codes?

The gRPC system itself has the ability to attach additional data to an error – and thus expand the context of the problem

import (
  "google.golang.org/grpc/status"
  "google.golang.org/grpc/codes"
  "google.golang.org/genproto/googleapis/rpc/errdetails"
)

// ...

st := status.New(codes.InvalidArgument, "invalid parameter")
// Общая форма ошибки
errInfo := &errdetails.ErrorInfo{
	Reason: "Не хватает средств на счету",
	Domain: "finance",
	Metadata: map[string]string{
		"my_meta_info": "my_meta_details",
	},
}

st, err := st.WithDetails(errInfo)
if err != nil {
	return fmt.Sprintf("st.WithDetails: %w", err)
}

return st.Err()

But in cases where you want to get more detailed errors – for example, with clarification of the problematic field. In this case, you can use the BadRequest type and write more details about the error.

Defining and Using a Custom Error

But! What if the standard details options don't work? We can make our own error types! 🙂

First, let's define a custom error in proto file. We need to create message CustomErrorDetail errors. It will contain information about errors related to custom data:

syntax = "proto3";

package myerrors;

message CustomErrorDetail {
  string reason = 1;
  string field = 2;
  string help = 3;
}

Now that we have a custom error definition, we can use it to pass more specific and detailed error information. This is especially useful when we want to pinpoint the specific fields or parameters that caused the error. Creating and using such aCustomErrorDetailin the server code allows not only to report problems, but also to provide the client with recommendations on how to fix them, which makes the interaction more transparent and efficient.

import (
  "google.golang.org/grpc/status"
  "google.golang.org/grpc/codes"
  "google.golang.org/protobuf/types/known/anypb"
  "myerrors"
)

// ...

customErrorDetail := &myerrors.CustomErrorDetail{
    Reason: "Value out of range",
    Field: "age",
    Help: "The age must be between 0 and 120",
}

st := status.New(codes.InvalidArgument, "invalid parameter")
st, err = st.WithDetails(customErrorDetail)
if err != nil {
    return fmt.Sprintf("Unexpected error attaching custom error detail: %w", err)
}

return st.Err()

Working from the client side

Now let's look at how the client side will interact with the gRPC error handling system we described earlier.

Standard Error Handling

When a client receives a response from a gRPC server, it can check for errors using standard gRPC mechanisms, such as:

import (
  "context"
  "google.golang.org/grpc"
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
  "log"
)н

func main() {
  conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
  if err != nil {
    log.Fatalf("did not connect: %v", err)
  }
  defer conn.Close()

  client := NewYourServiceClient(conn)
  response, err := client.YourMethod(context.Background(), &YourRequest{})
  if err != nil {
    st, ok := status.FromError(err)
    if ok {
      switch st.Code() {
      case codes.InvalidArgument:
        log.Println("Invalid argument error:", st.Message())
      case codes.NotFound:
        log.Println("Not found error:", st.Message())
      // Обработайте другие коды ошибок по необходимости
      default:
        log.Println("Unexpected error:", st.Message())
      }
    } else {
      log.Fatalf("failed to call YourMethod: %v", err)
    }
  } else {
    log.Println("Response:", response)
  }
}

Extracting additional error details

Now comes the most interesting part: in order for the client side to be able to extract details for analysis, we need to process these very details.

Here's how you can do it:

import (
  "google.golang.org/grpc/status"
  "google.golang.org/genproto/googleapis/rpc/errdetails"
  "myerrors"
  "log"
)

// ...

func handleError(err error) {
  st, ok := status.FromError(err)
  if !ok {
    log.Fatalf("An unexpected error occurred: %v", err)
  }

  for _, detail := range st.Details() {
    switch t := detail.(type) {
    case *errdetails.BadRequest:
      // Обработка деталей неверного запроса
      for _, violation := range t.GetFieldViolations() {
        log.Printf("The field %s was wrong: %s\\\\n", violation.GetField(), violation.GetDescription())
      }
    case *myerrors.CustomErrorDetail:
      // Обработка кастомных деталей ошибок
      log.Printf("Custom error detail: Reason: %s, Field: %s, Help: %s\\\\n", t.Reason, t.Field, t.Help)
    // Добавьте обработку других типов ошибок по необходимости
    default:
      log.Printf("Received an unknown error detail type: %v\\\\n", t)
    }
  }
}

Conclusion

We looked at how to use standard gRPC error codes, how to add additional data to errors, and how to create and handle custom errors. These approaches allow for a more flexible and granular approach to error handling, which is especially important for complex systems where a simple error message may not be enough.

When designing an API, it is important to remember that the client side should be able to easily and unambiguously interpret server responses. Using standard gRPC error mechanisms helps achieve this goal, improving the interaction between the client and server and making the system as a whole more robust and understandable.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *