chix 是一个 chi-first、同时保持 net/http 兼容的轻量 HTTP 边界库,用来把 JSON API 的公开行为收敛为一套稳定、可预期的约定。
- 统一成功响应与错误响应的 JSON 结构
- 集中映射、观测并写回显式边界错误,错误观测关联
request_id - 兼容
go-chi与标准http.Handler中间件链 - 兼容原生
net/http/ServeMux,不依赖第三方框架运行时 - 不接管 router,不绑定项目结构,也不引入新的框架运行时
- 提供请求体解码、query 解码和输入校验封装
- 默认采用 fail-closed 策略,响应一旦开始写出就不再改写
- 你在使用
chi,想统一成功响应、错误响应和请求校验行为 - 你希望 middleware、handler、service 继续沿用标准
net/http风格 - 你想把内部错误稳定地映射为公开错误,而不是在深层直接写 HTTP 响应
chix 负责这些事:
- 统一成功响应和显式边界错误的 JSON 契约
- 把
Error(...)/WriteError(...)送进来的错误集中映射、观测和写回 - 提供请求解码、查询参数解码和输入校验封装
chix 不负责这些事:
- 接管或包装你的 router
- 接管系统级 panic recovery
- 提供 body / query / header / path 的全量绑定运行时
- 承诺所有
500都返回统一 JSON,panic 或外层 recoverer 的行为不属于chix契约
- 在外层安装一次
HandleErrors(...) - 用
Decode*/Validate处理请求输入 - 用
Error(...)把失败送进统一错误出口 - 用
Respond/RespondWithMeta/RespondEmpty显式写回成功结果
HandleErrors(...)是统一错误处理的唯一中心Error(...)只负责“送错”,不负责立即写回Respond*只负责成功响应- panic 不属于
chix的默认职责,应由外层 recoverer 处理 - 如果响应已经开始写回,
chix不会为了“统一格式”去改写已经发出的结果
如果你只记四组名字,记住这些就够了:
HandleErrorsErrorRespond*Decode*/Validate
chix 当前要求 Go 1.24+。
go get github.com/kanata996/chix下面是推荐的 chi 接法:外层 recoverer 负责 panic,chix 负责显式错误的统一处理。
package main
import (
"errors"
"log"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/kanata996/chix"
)
var errUserNotFound = errors.New("user not found")
const codeUserNotFound = "user_not_found"
func mapUserError(err error) *chix.HTTPError {
if errors.Is(err, errUserNotFound) {
return chix.NotFound(codeUserNotFound, "user not found")
}
return nil
}
func main() {
r := chi.NewRouter()
r.Use(middleware.Recoverer)
r.Use(chix.HandleErrors())
r.NotFound(chix.NotFoundHandler())
r.MethodNotAllowed(chix.MethodNotAllowedHandler())
r.Route("/users", func(r chi.Router) {
r.Use(chix.UseErrorMappers(mapUserError))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
var query struct {
ID string `query:"id"`
}
if chix.Error(r, chix.DecodeQuery(r, &query)) {
return
}
if query.ID == "missing" {
chix.Error(r, errUserNotFound)
return
}
if err := chix.Respond(w, http.StatusOK, map[string]any{
"id": query.ID,
}); err != nil {
chix.Error(r, err)
}
})
})
log.Fatal(http.ListenAndServe(":8080", r))
}接入建议:
chi项目把 router 级 recoverer 放在HandleErrors(...)外层,例如middleware.Recoverer- 通常只在 router 的 composition root 安装一次
HandleErrors(...) - 各 feature 可在各自的 route group 上用
UseErrorMappers(...)注入业务错误映射 - handler 和普通 middleware 都使用
if chix.Error(r, err) { return } chi的NotFound/MethodNotAllowed直接接chix.NotFoundHandler()/chix.MethodNotAllowedHandler()chix生成的兜底 request id 只保证错误链路内稳定,不替代外层 access log / tracing 的 request id 策略chix支持设置自定义 request id,通过chix.SetRequestID(...)注入给chix
如果你继续使用标准库,也有 ServeMux 版本示例:
chix 对外只定义三种由自身写出的响应形态:
- 带响应体的成功响应
- 带响应体的错误响应
- 不带响应体的空响应
成功响应:
{
"data": {},
"meta": {}
}错误响应:
{
"error": {
"code": "invalid_request",
"message": "request contains invalid fields",
"details": []
}
}约束:
data必须存在,且不能编码成nullmeta没内容时可省略,如果存在,必须编码成 JSON objecterror.code必须是稳定机器码chix通过errcode子包公开了一组常见 code 常量,见下文;业务错误也可以继续使用你自己定义的稳定字符串常量error.message必须是可安全公开的文案error.details始终存在,没有内容时输出[]Respond/RespondWithMeta只能用于允许携带响应体的成功状态码RespondEmpty用于不带响应体的成功响应
这个契约只覆盖下面这些入口写出的响应:
Respond(...)RespondWithMeta(...)RespondEmpty(...)HandleErrors(...)WriteError(...)
如果请求因 panic 或 router / framework 级 recoverer 返回 500,是否带响应体、响应体长什么样,都不属于 chix 契约。
chix 的错误处理模型不是“在各处直接写错误响应”,而是“让错误在链路中短路返回,并在边界统一定型”。
核心 API:
type Option func(*config)
func HandleErrors(opts ...Option) func(http.Handler) http.Handler
func WriteError(w http.ResponseWriter, r *http.Request, err error, opts ...Option) bool
func Error(r *http.Request, err error) bool
func NotFoundHandler() http.HandlerFunc
func MethodNotAllowedHandler() http.HandlerFunc使用约束:
Error(...)依赖当前请求已经安装HandleErrors(...)- 如果没有安装 middleware,却仍然需要一次性的立即写错,应显式使用
WriteError(...) - 对
Error(...)传入nil会返回false - 如果在没有
HandleErrors(...)的请求里调用Error(...),这会被视为配置错误并触发 panic
公开错误类型:
type HTTPError struct {
// unexported fields
}
func NewHTTPError(status int, code, message string, details ...any) *HTTPError
func BadRequest(code, message string, details ...any) *HTTPError
func Unauthorized(code, message string, details ...any) *HTTPError
func Forbidden(code, message string, details ...any) *HTTPError
func NotFound(code, message string, details ...any) *HTTPError
func MethodNotAllowed(code, message string, details ...any) *HTTPError
func Conflict(code, message string, details ...any) *HTTPError
func Gone(code, message string, details ...any) *HTTPError
func UnprocessableEntity(code, message string, details ...any) *HTTPError
func TooManyRequests(code, message string, details ...any) *HTTPError
func (e *HTTPError) Error() string
func (e *HTTPError) Status() int
func (e *HTTPError) Code() string
func (e *HTTPError) Message() string
func (e *HTTPError) Details() []any常见公开 code 常量统一放在 errcode 子包:
import "github.com/kanata996/chix/errcode"errcode可复用的公共 code,主要覆盖协议/边界错误、请求解码与校验错误,以及少量跨业务的泛语义错误- 为避免过早把完整枚举承诺成稳定契约,README 不再列出全部常量,请以当前版本的
errcode包定义为准 - 这些常量只是便捷入口,
NewHTTPError(...)、NotFound(...)等 helper 仍然接受任意自定义稳定字符串 - 业务错误建议自己定义常量,只有像
resource_not_found这类确实跨业务复用的语义,才值得回收到errcode
约束:
4xx表达客户端侧失败5xx表达服务端侧失败NewHTTPError(...)会把非法状态码保守规范化为500- 常见
4xx可以直接用 helper:BadRequest(...)、Unauthorized(...)、Forbidden(...)、NotFound(...)、MethodNotAllowed(...)、Conflict(...)、Gone(...)、UnprocessableEntity(...)、TooManyRequests(...) - 当
code/message为空时,会按状态码族补默认值 - 未识别错误默认回退为
500 internal_error
错误映射与观测:
type ErrorMapper func(err error) *HTTPError
func WithErrorMappers(mappers ...ErrorMapper) Option
func UseErrorMappers(mappers ...ErrorMapper) func(http.Handler) http.Handler
type ErrorReport struct {
Request *http.Request
Error error
PublicError *HTTPError
Stage string
RequestID string
ResponseStarted bool
}
type ErrorReporter func(ErrorReport)
func WithErrorReporter(reporter ErrorReporter) Option
func SetRequestID(r *http.Request, id string) *http.RequestErrorReport.Stage是内部观测字段,当前稳定值包括decode、validate、routing、processing、write_responseErrorReport.RequestID表示当前错误观测实际使用的 request id;优先使用SetRequestID(...)注入的值- 如果调用方没有显式设置 request id,
chix会在第一次发送错误观测时自动生成一个,并在同一次错误处理链路里复用 processing表示请求已进入业务处理链,覆盖 handler、service、repository 这段内部处理范围- 当错误响应在序列化或写回阶段再次失败时,
chix会额外发送一条Stage == "write_response"的内部观测,并保守回退为500 internal_error - 默认 stderr logger 会记录
5xx,并追加当前 goroutine 的 stack;401/403会记录为安全审计日志;普通4xx默认不单独记错误日志 WithErrorReporter(nil)会关闭chix的错误观测
RequestID 接入建议:
- 默认不需要额外安装 request id middleware;缺失时
chix会在错误路径内部自动生成并复用 request id - 如果项目已经有 request id 机制,可在应用层取出该值,再通过
chix.SetRequestID(...)注入给chix chi项目复用middleware.RequestID的接法见文末“复用chi的 Request ID”net/http项目可在自己的 middleware 里生成 request id,然后把返回的*http.Request继续向下传递- 不建议让
chix根包直接依赖某个框架的 request id context 约定 RequestID不是TraceID,两者不应混用
推荐做法:
- 把内部业务错误通过
ErrorMapper映射成公开边界错误 - 在启动期通过
WithErrorMappers(...)注册全局 mapper - 通过
UseErrorMappers(...)给 feature / route group 注入局部 mapper - 更内层的
UseErrorMappers(...)优先级高于更外层的UseErrorMappers(...) UseErrorMappers(...)的优先级高于WithErrorMappers(...)- 把鉴权失败、限流、参数错误这类边界错误优先写成
chix.Unauthorized(...)、chix.TooManyRequests(...)、chix.BadRequest(...)这类 helper;不常见状态再回退到chix.NewHTTPError(...)
在 feature-first 项目里,更推荐让 feature 在挂载路由时就注入 mapper,而不是在 composition root 里手动维护一长串集中注册。
func Respond(w http.ResponseWriter, status int, data any) error
func RespondWithMeta(w http.ResponseWriter, status int, data any, meta any) error
func RespondEmpty(w http.ResponseWriter, status int) errorRespond(...)返回{"data": ...}RespondWithMeta(...)返回{"data": ..., "meta": ...}RespondEmpty(...)只写状态码,不写响应体RespondWithMeta(...)的meta如果存在,必须编码成 JSON object
根包的请求输入 helper 是对 reqx 子包的一层轻量封装。推荐应用边界代码优先使用 chix.Decode* / chix.Validate,这样可以把依赖面收敛在 chix 根包。
公开 API:
type DecodeOption = reqx.DecodeOption
type QueryOption = reqx.QueryOption
type Violation = reqx.Violation
type ValidateFunc[T any] func(*T) []Violation
func WithMaxBodyBytes(limit int64) DecodeOption
func AllowUnknownFields() DecodeOption
func AllowEmptyBody() DecodeOption
func AllowUnknownQueryFields() QueryOption
func DecodeJSON[T any](r *http.Request, dst *T, opts ...DecodeOption) error
func DecodeAndValidateJSON[T any](r *http.Request, dst *T, fn ValidateFunc[T], opts ...DecodeOption) error
func DecodeQuery[T any](r *http.Request, dst *T, opts ...QueryOption) error
func DecodeAndValidateQuery[T any](r *http.Request, dst *T, fn ValidateFunc[T], opts ...QueryOption) error
func Validate[T any](dst *T, fn ValidateFunc[T]) error这些 helper 会把请求解码和输入校验错误适配到 chix 的统一错误响应中。
它们的职责边界是:
- 负责 JSON 请求体解码
- 负责 URL query 解码
- 负责把
[]Violation归一化成稳定的422 invalid_request
它们不负责:
- 拥有 router
- 接管完整响应生命周期
- path param / header / form 的全量 binding runtime
- 业务规则建模本身
如果你明确想直接依赖更窄的子包,也可以直接使用 reqx。reqx.Problem 仍然可以通过 Error(...) / WriteError(...) 归一化到 chix 的统一公开错误契约中。
推荐先看 chi 示例,再根据自己的项目接法选择 net/http 版本:
_examples/chi:推荐主路径_examples/nethttp:标准库兼容示例
两个目录都是独立 Go module,可以直接运行:
cd _examples/chi
go test ./...
go run .
cd ../nethttp
go test ./...
go run .仓库根目录常用命令:
make test
make test-cover
make test-race
make bench
make cichix 默认会在错误链路里惰性生成并复用 request id。只有当你希望 ErrorReport.RequestID 与 chi/middleware.RequestID、access log 或网关日志对齐时,才需要显式桥接。
func bindChiRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
r = chix.SetRequestID(r, reqID)
}
next.ServeHTTP(w, r)
})
}
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(bindChiRequestID)
r.Use(middleware.Recoverer)
r.Use(chix.HandleErrors())
}说明:
chi负责生成或传播 request id- bridge middleware 负责把应用最终采用的值显式注入给
chix chix根包因此不需要依赖chi的 context 协议
Error(...)依赖当前请求已经安装HandleErrors(...);如果没有安装 middleware 仍然调用,会按配置错误处理并触发 panicRespond(...)/RespondWithMeta(...)的成功载荷仍然要求可被标准库encoding/json正常编码;错误响应里的details如果混入不可编码值,会在内部观测中记录并降级为details: [],而不是把整个公开错误改写成500reqx.DecodeJSON(...)当前会在受WithMaxBodyBytes(...)约束下先完整读取请求体,再执行 JSON decode;这适合常规 JSON API 请求,但不是面向超大 body 或流式 decode 的运行时Respond(...)/RespondWithMeta(...)是一次性 envelope writer,不提供流式响应能力SetRequestID(...)只负责把 request id 桥接进chix的错误观测链路,不替代外层 access log、trace 或分布式链路追踪策略
本项目当前的公开兼容边界由本文档中描述的两类内容构成:
- 根包公开 API
- 由
chix自己写出的 HTTP 可观察行为
版本策略:
- 在
v1.0.0之前,minor release 仍可能包含破坏性调整,但会在 CHANGELOG 中明确标注 - 在
v1.0.0之后,破坏根包公开 API 或 HTTP 契约的变更应只出现在新的 major version