// 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 }