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>
117 lines
2.9 KiB
Go
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
|
|
}
|