跳转至

状态码体系与 Rich Error 富错误模型

在分布式系统中,错误绝不能仅仅是一个模糊的字符串。良好的错误处理体系必须能让客户端区分重试性错误业务逻辑违例身份权限拒绝

gRPC 提供了跨语言一致的标准状态码(Canonical Status Codes)以及企业级的 Rich Error Model(富错误模型)


1. 17 种标准状态码速查表

gRPC 定义了从 016 共 17 个标准化状态码:

状态码 英文标识 对应 HTTP 映射 语义说明与典型场景 是否可重试
0 OK 200 请求成功完成 -
1 CANCELLED 499 操作被调用方显式取消(如客户端关闭) 视情况
2 UNKNOWN 500 未知服务器端异常(通常为未捕获的 panic 或代码 bug)
3 INVALID_ARGUMENT 400 客户端请求参数不合法(如字段缺失、格式错误) ❌ 否
4 DEADLINE_EXCEEDED 504 请求已超出设定的截止时间(Deadline) ⚠️ 需幂等保障
5 NOT_FOUND 404 请求的资源不存在 ❌ 否
6 ALREADY_EXISTS 409 尝试创建的资源已存在 ❌ 否
7 PERMISSION_DENIED 403 调用方无权限执行该操作(身份已知但权限不足) ❌ 否
8 RESOURCE_EXHAUSTED 429 资源耗尽(如限流 Rate Limit、存储配额满) ⚠️ 指数退避重试
9 FAILED_PRECONDITION 400 前置条件未满足(如删除非空目录) ❌ 否
10 ABORTED 409 并发冲突被中止(如事务锁冲突) ✅ 是
11 OUT_OF_RANGE 400 尝试在有效范围之外操作(如超出分页最大边界) ❌ 否
12 UNIMPLEMENTED 501 服务端未实现该方法或未开启该服务路由 ❌ 否
13 INTERNAL 500 严重的底层内部系统故障(如硬件损坏、不可恢复的底层状态) ❌ 否
14 UNAVAILABLE 503 服务当前不可用(网络抖动、服务正在重启、连接断开) ✅ 推荐重试
15 DATA_LOSS 500 不可恢复的数据损坏或丢失 ❌ 否
16 UNAUTHENTICATED 401 缺少合法身份凭证(未登录或 Token 过期) ❌ 需重新认证

2. Google Rich Error Model 富错误模型

原生的 grpc-statusgrpc-message 仅能表达简单的错误码和纯文本。为了支持更精细化的结构化错误信息(例如:指出表单中哪几个字段校验失败、重试退避间隔多长),Google 定义了基于 Protobuf 的 Rich Error Model

google.rpc.Status 原型

package google.rpc;

message Status {
  int32 code = 1;        // 对应 gRPC Canonical 状态码
  string message = 2;    // 开发者可读的错误概要
  repeated google.protobuf.Any details = 3; // 强类型错误扩展对象
}

官方标准错误详情扩展类型 (google/rpc/error_details.proto)

  • RetryInfo:通知客户端在多长时间后可重试。
  • BadRequest:详细指出是哪个字段校验失败以及失败规则(FieldViolation)。
  • QuotaFailure:指出哪项配额(API 调用次数、并发数)超标。
  • ErrorInfo:跨系统标准化错误原因、域名与元数据(Cloud 原生通用)。
  • ResourceInfo:指明发生错误的目标资源类型与资源名称。

3. 传输层实现:grpc-status-details-bin

富错误模型在传输层无需对 HTTP/2 做任何破坏性修改:服务端将整个 google.rpc.Status 消息序列化为 Protobuf 二进制,并在响应的 Trailer 帧中写入自定义二进制头:

grpc-status: 3
grpc-message: Invalid arguments supplied
grpc-status-details-bin: <Base64-Encoded google.rpc.Status protobuf>
gRPC 客户端 SDK 内部检测到该 Header 时,会自动完成 Base64 解码与反序列化。


4. 实战代码:构造与解析 Rich Error

package main

import (
    "context"
    "fmt"
    "time"

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

// 服务端返回结构化富错误
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
    if req.GetEmail() == "" {
        st := status.New(codes.InvalidArgument, "用户参数校验不通过")

        // 添加具体的字段错误详情
        vDetail := &errdetails.BadRequest{
            FieldViolations: []*errdetails.BadRequest_FieldViolation{
                {
                    Field:       "email",
                    Description: "email 不能为空且必须为标准邮件格式",
                },
            },
        }

        // 挂载到 status 对象中
        stWithDetails, err := st.WithDetails(vDetail)
        if err == nil {
            return nil, stWithDetails.Err()
        }
        return nil, st.Err()
    }
    return &pb.User{...}, nil
}

// 客户端解析富错误
func handleClientError(err error) {
    st := status.Convert(err)
    fmt.Printf("错误码: %s, 信息: %s\n", st.Code(), st.Message())

    // 提取并强转错误详情
    for _, detail := range st.Details() {
        switch t := detail.(type) {
        case *errdetails.BadRequest:
            for _, violation := range t.GetFieldViolations() {
                fmt.Printf("-> 字段 [%s] 校验失败: %s\n", violation.GetField(), violation.GetDescription())
            }
        case *errdetails.RetryInfo:
            fmt.Printf("-> 建议重试退避时间: %v\n", t.GetRetryDelay())
        }
    }
}