状态码体系与 Rich Error 富错误模型¶
在分布式系统中,错误绝不能仅仅是一个模糊的字符串。良好的错误处理体系必须能让客户端区分重试性错误、业务逻辑违例与身份权限拒绝。
gRPC 提供了跨语言一致的标准状态码(Canonical Status Codes)以及企业级的 Rich Error Model(富错误模型)。
1. 17 种标准状态码速查表¶
gRPC 定义了从 0 到 16 共 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-status 和 grpc-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>
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())
}
}
}