Go Action: Error Handling In gRPC
Tags: go, grpc
Published: 2024-05-11 08:14:07

gRPC generally avoids defining errors within messages. After all, each gRPC service inherently comes with an error return value, serving as a dedicated channel for error transmission. All error returns in gRPC should either be nil or **an error generated by status.Status**. This ensures that errors can be directly recognized by the calling Client.

## 1. Basic Usage
Simply returning upon encountering a Go error won't be recognizable by downstream clients. The proper approach is:
- Call the status.New method and pass an appropriate error code to generate a status.Status object.
- Call the status.Err method to generate an error recognizable by the calling party, then return.

```go
st := status.New(codes.NotFound, "some description")  
err := st.Err()
```

The error code passed in is of type codes.Code. Alternatively, you can use status.Error for a more convenient method that eliminates manual conversion.

```go
err := status.Error(codes.NotFound, "some description")
```

# 2. Advanced Usage
The aforementioned errors have a limitation: the error codes defined by code.Code only cover certain scenarios and **cannot comprehensively express the error scenarios encountered in business**.

gRPC provides a mechanism to supplement information within errors: the status.WithDetails method.

Clients can directly retrieve the contents by converting the error back to status.Status and using the status.Details method.

status.Details returns a slice, which is a slice of interface{}. However, Go automatically performs type conversion, allowing direct usage through assertion.

### Server-Side Example

- Generate a status.Status object
- Populate additional error information

```go
func ErrorWithDetails() error {  
    st := status.Newf(codes.Internal, fmt.Sprintf("something went wrong: %v", "api.Getter"))  
    v := &errdetails.PreconditionFailure_Violation{ //errDetails  
       Type:        "test",  
       Subject:     "12",  
       Description: "32",  
    }  
    br := &errdetails.PreconditionFailure{}  
    br.Violations = append(br.Violations, v)  
    st, _ = st.WithDetails(br)  
    return st.Err()  
}
```

### Client-Side Example
- Parse error information after RPC error
- Retrieve error details directly through assertion

```go
resp, err := odinApp.CreatePlan(cli.StaffId.AssetId, gentRatePlanMeta(cli.StaffId))  
​  
  if status.Code(err) != codes.InvalidArgument {  
    logger.Error("create plan error:%v", err)  
  } else {  
    for _, d := range status.Convert(err).Details() {  
      //   
      switch info := d.(type) {  
      case *errdetails.QuotaFailure:  
        logger.Info("Quota failure: %s", info)  
      case *errdetails.PreconditionFailure:  
        detail := d.(*errdetails.PreconditionFailure).Violations  
        for _, v1 := range detail {  
          logger.Info(fmt.Sprintf("details: %+v", v1))  
        }  
      case *errdetails.ResourceInfo:  
        logger.Info("ResourceInfo: %s", info)  
  
      case *errdetails.BadRequest:  
        logger.Info("ResourceInfo: %s", info)  
  
      default:  
        logger.Info("Unexpected type: %s", info)  
      }  
    }  
  }  
  logger.Infof("create plan success,resp=%v", resp)
```

## Principles
How are these errors passed to the calling Client? They are placed in metadata, which is then placed in the HTTP header. Metadata is in the format of key-value pairs. In error transmission, the key is a fixed value: grpc-status-details-bin. The value is encoded by proto and is binary-safe. Most languages have implemented this mechanism.

![Pasted image 20240511155439](https://images.hxzhouh.com/blog-images/2024/05/28177eb7de8b02e73d9b3b0484e91e5a.png)

### Note
gRPC imposes restrictions on response headers, with a limit of 8K, so errors should not be too large.
## Reference:
[Protocol Buffers Tutorial](https://protobuf.dev/getting-started/gotutorial/)
[errdetails](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/errdetails)