跳转到内容

错误模型

每个 Plumego handler 都用同两个函数返回错误:contract.WriteErrorcontract.NewErrorBuilder。这条唯一的写入路径保证所有错误响应具有一致的 JSON 格式,客户端可以可靠地解析。

{
"error": {
"type": "required_field_missing",
"code": "REQUIRED",
"message": "name is required",
"category": "validation_error",
"details": { "field": "name" }
},
"request_id": "0jx9f3kp2q"
}

request_idmiddleware/requestid 运行后自动注入,无需手动设置。

使用 NewErrorBuilder,绝不要直接构造 APIError 字面量:

import "github.com/spcent/plumego/contract"
err := contract.NewErrorBuilder().
Type(contract.TypeRequired). // 自动设置 type、code、category、HTTP 状态
Detail("field", "name"). // 可选:向 details 映射添加字段
Message("name is required"). // 覆盖默认的人类可读文本
Build()
contract.WriteError(w, r, err)

.Type() 是唯一必须调用的步骤,它自动填充 typecodecategory 和 HTTP 状态码。只有在默认文本不够时才需要覆盖 MessageDetail

常量HTTPcodecategory
TypeValidation400VALIDATION_ERRORvalidation_error
TypeRequired400REQUIRED_FIELD_MISSINGvalidation_error
TypeInvalidFormat400INVALID_FORMATvalidation_error
TypeOutOfRange400VALUE_OUT_OF_RANGEvalidation_error
TypeDuplicate400DUPLICATE_VALUEvalidation_error
TypeUnauthorized401UNAUTHORIZEDauth_error
TypeInvalidToken401INVALID_TOKENauth_error
TypeExpiredToken401EXPIRED_TOKENauth_error
TypeForbidden403FORBIDDENauth_error
TypeNotFound404RESOURCE_NOT_FOUNDclient_error
TypeConflict409RESOURCE_CONFLICTclient_error
TypeAlreadyExists409RESOURCE_ALREADY_EXISTSclient_error
TypeGone410RESOURCE_GONEclient_error
TypeRateLimited429RATE_LIMITEDrate_limit_error
TypeInternal500INTERNAL_ERRORserver_error
TypeUnavailable503SERVICE_UNAVAILABLEserver_error
TypeTimeout408TIMEOUTtimeout_error
TypeMethodNotAllowed405METHOD_NOT_ALLOWEDclient_error
TypeNotImplemented501NOT_IMPLEMENTEDserver_error
TypeBadGateway502BAD_GATEWAYserver_error
TypeGatewayTimeout504GATEWAY_TIMEOUTtimeout_error
TypeMaintenance503MAINTENANCE_MODEserver_error

category 字段将错误分组,便于可观测性和告警:

含义
client_error4xx — 客户端输入有误
server_error5xx — 基础设施或服务器逻辑故障
validation_error输入验证失败(client_error 的子集)
auth_error认证或授权失败
rate_limit_error触发限流
timeout_error超时
func (h ItemHandler) Create(w http.ResponseWriter, r *http.Request) {
var req CreateItemRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeValidation).
Message(err.Error()).
Build())
return
}
if req.Name == "" {
contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeRequired).
Detail("field", "name").
Build())
return
}
item, err := h.svc.Create(r.Context(), req)
if errors.Is(err, ErrAlreadyExists) {
contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeAlreadyExists).
Message("item with this name already exists").
Build())
return
}
if err != nil {
contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeInternal).
Build())
return
}
contract.WriteResponse(w, r, http.StatusCreated, item, nil)
}

需要同时返回多个字段错误时,使用 Details 映射携带:

errs := map[string]string{}
if req.Name == "" { errs["name"] = "required" }
if req.Email == "" { errs["email"] = "required" }
if len(errs) > 0 {
b := contract.NewErrorBuilder().
Type(contract.TypeValidation).
Message("request validation failed")
for field, msg := range errs {
b = b.Detail(field, msg)
}
contract.WriteError(w, r, b.Build())
return
}