跳转到内容

处理错误

本指南展示如何使用 contract.WriteErrorcontract.NewErrorBuilder 从 Plumego handler 返回结构化错误响应。系统中的每个 handler 都使用这两个函数作为唯一的错误写入路径。

边界原理请参见 Contract Primer

  • APIError 的形状及其如何序列化
  • NewErrorBuilder 构造错误
  • 内置 ErrorType 目录及何时使用每个条目
  • 在 handler 中一致地返回错误

每个错误响应都有相同的 JSON 信封:

{
"error": {
"type": "required_field_missing",
"code": "REQUIRED",
"message": "name is required",
"category": "validation_error",
"severity": "error",
"details": { "field": "name" },
"request_id": "abc-123"
}
}

request_idrequestid middleware 运行后自动注入。

第一步 — 对所有错误响应使用 NewErrorBuilder

Section titled “第一步 — 对所有错误响应使用 NewErrorBuilder”

永远不要将 APIError 构造为结构体字面量。使用 builder:

import "github.com/spcent/plumego/contract"
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeRequired).
Detail("field", "name").
Message("name is required").
Build())

Type 根据 ErrorType 常量自动设置 typecodecategory 和 HTTP 状态码。你只需要覆盖 Message,可选地添加 Detail

情况使用的 ErrorType
必填的 query param 或 body 字段缺失contract.TypeRequired
输入值格式错误contract.TypeInvalidFormat
输入值超出允许范围contract.TypeOutOfRange
通过 ID 标识的资源不存在contract.TypeNotFound
请求未认证contract.TypeUnauthorized
已认证用户缺少权限contract.TypeForbidden
与现有状态冲突(如重复)contract.TypeConflict
非客户端原因的服务端故障contract.TypeInternal
func (h APIHandler) Greet(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeRequired).
Detail("field", "name").
Message("name is required").
Build())
return
}
_ = contract.WriteResponse(w, r, http.StatusOK,
greetResponse{Message: "hello, " + name}, nil)
}

WriteError 之后始终 return。响应已经写出;继续执行 handler 会产生未定义行为。

第四步 — 将领域错误传递到传输层

Section titled “第四步 — 将领域错误传递到传输层”

将领域错误类型保留在领域包中,在 handler 中将其转换为 APIError

func (h *ItemHandler) Get(w http.ResponseWriter, r *http.Request) {
item, err := h.Repo.Find(r.Context(), id)
switch {
case errors.Is(err, repo.ErrNotFound):
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeNotFound).
Message("item not found").
Build())
case err != nil:
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeInternal).
Message("could not load item").
Build())
default:
_ = contract.WriteResponse(w, r, http.StatusOK, item, nil)
}
}

Handler 负责转换;领域层保持干净。

第 5 步 — 包装多种领域错误类型

Section titled “第 5 步 — 包装多种领域错误类型”

对于需要处理多种错误情况的 handler,用一个辅助函数将转换逻辑集中在一处:

func writeRepoError(w http.ResponseWriter, r *http.Request, err error, resourceName string) {
switch {
case errors.Is(err, repo.ErrNotFound):
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeNotFound).
Message(resourceName + " not found").
Build())
case errors.Is(err, repo.ErrDuplicate):
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeAlreadyExists).
Message(resourceName + " already exists").
Build())
default:
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeInternal).
Message("could not load " + resourceName).
Build())
}
}
// 在 handler 中使用:
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.Repo.Find(r.Context(), id)
if err != nil {
writeRepoError(w, r, err, "user")
return
}
_ = contract.WriteResponse(w, r, http.StatusOK, user, nil)
}

将转换函数放在应用代码中,而不是共享库里——每个服务的领域错误是各自独有的。

  • 系统中每个错误只有一条序列化路径 — 没有每个 handler 各自的错误信封变体。
  • HTTP 状态码从 ErrorType 自动推导;不会意外为错误发送 200。
  • request_id 无需 handler 任何操作即可注入每个错误响应。
  • Builder 防止构造不完整的 APIError 值逃逸到响应中。
现象先检查
错误返回了 HTTP 200失败路径使用 contract.WriteError,不要用 contract.WriteResponse
错误响应形态不对contract.NewErrorBuilder().Type(...).Build() 构造,再交给 WriteError 写出
handler 写了两次响应WriteError 之后立即 return
缺少 request_id确认 middleware/requestid 在 handler 之前运行
错误类型过于宽泛优先选择 TypeRequiredTypeInvalidFormatTypeOutOfRange,再退回 TypeValidation

参考服务的 handler 文件展示了 WriteErrorNewErrorBuilder 在完整可运行 handler 中的用法: