Files
weichenandCursor bd53e9ba79
ci / vet-and-test (push) Successful in 3s
ci / docker-checkout-hostgateway (push) Successful in 3s
Add a shared error-code catalog so failures can be traced by stable codes.
Services can resolve legacy aliases and expose the catalog over CLI and HTTP instead of ad-hoc error strings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 13:43:12 +00:00

117 lines
2.9 KiB
Go

// Package httpx 把错误码写成统一 HTTP 信封,并透传追踪标识。
package httpx
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strings"
"unicode"
"error-code-system/errcode"
)
// TraceHeader 是跨系统传递追踪标识的请求/响应头。
const TraceHeader = "X-Trace-Id"
type traceKey struct{}
// Envelope 是对外错误体。成功响应不使用此结构。
type Envelope struct {
Error Body `json:"error"`
}
// Body 是错误对象。code 恒为规范码;未知输入已回落 sys.unknown_code。
type Body struct {
Code string `json:"code"`
Message string `json:"message"`
Reason string `json:"reason,omitempty"`
TraceID string `json:"trace_id,omitempty"`
}
// Trace 为每个请求保证一个合法的追踪标识:沿用合法的请求头,否则生成。
// 标识同时写回响应头与请求上下文。
func Trace(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := sanitizeTrace(r.Header.Get(TraceHeader))
if id == "" {
id = newTraceID()
}
w.Header().Set(TraceHeader, id)
ctx := context.WithValue(r.Context(), traceKey{}, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// TraceID 读取中间件写入的追踪标识,其次读取合法请求头。
func TraceID(r *http.Request) string {
if r == nil {
return ""
}
if id, ok := r.Context().Value(traceKey{}).(string); ok && id != "" {
return id
}
return sanitizeTrace(r.Header.Get(TraceHeader))
}
// WriteError 按注册表把 err 写成 JSON 信封并设置 HTTP 状态码。
// reg 为空时使用 errcode.Default。非 *errcode.E 视为 sys.internal,原文放入 reason。
func WriteError(reg *errcode.Registry, w http.ResponseWriter, r *http.Request, err error) {
if reg == nil {
reg = errcode.Default
}
code := errcode.SysInternal
reason := ""
if err != nil {
reason = err.Error()
}
if e := errcode.From(err); e != nil {
code = e.Code
reason = e.Reason
if reason == "" && e.Unwrap() != nil {
reason = e.Unwrap().Error()
}
}
def := reg.Resolve(string(code))
traceID := TraceID(r)
if traceID != "" {
w.Header().Set(TraceHeader, traceID)
}
writeEnvelope(w, def.HTTP, Envelope{Error: Body{
Code: string(def.Code),
Message: def.Summary,
Reason: reason,
TraceID: traceID,
}})
}
func writeEnvelope(w http.ResponseWriter, status int, env Envelope) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(env)
}
func newTraceID() string {
var buf [16]byte
if _, err := rand.Read(buf[:]); err != nil {
return "trace-unavailable"
}
return hex.EncodeToString(buf[:])
}
func sanitizeTrace(s string) string {
s = strings.TrimSpace(s)
if len(s) < 8 || len(s) > 128 {
return ""
}
for _, r := range s {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == '.' || r == ':' {
continue
}
return ""
}
return s
}