Add a shared error-code catalog so failures can be traced by stable codes.
ci / vet-and-test (push) Successful in 3s
ci / docker-checkout-hostgateway (push) Successful in 3s

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>
This commit is contained in:
weichen
2026-09-29 13:43:12 +00:00
co-authored by Cursor
commit bd53e9ba79
33 changed files with 2489 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
// 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
}
+86
View File
@@ -0,0 +1,86 @@
package httpx
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"error-code-system/domains"
"error-code-system/errcode"
)
func TestTraceGeneratesAndEchoes(t *testing.T) {
var seen string
h := Trace(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen = TraceID(r)
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Header().Get(TraceHeader) == "" || seen != rec.Header().Get(TraceHeader) {
t.Fatalf("generated trace header=%q seen=%q", rec.Header().Get(TraceHeader), seen)
}
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(TraceHeader, "trace-from-caller")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Header().Get(TraceHeader) != "trace-from-caller" {
t.Fatalf("echo = %s", rec.Header().Get(TraceHeader))
}
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(TraceHeader, "bad id")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Header().Get(TraceHeader) == "bad id" || rec.Header().Get(TraceHeader) == "" {
t.Fatal("invalid trace should be replaced")
}
}
func TestWriteErrorEnvelope(t *testing.T) {
reg := errcode.NewRegistry()
domains.RegisterAll(reg)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(TraceHeader, "trace-abc-1234")
rec := httptest.NewRecorder()
err := errcode.Wrap(errors.New("dial timeout"), errcode.AgentProviderUnavailable, "模型不可达", "model", "glm-4")
WriteError(reg, rec, req, err)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status %d", rec.Code)
}
var env Envelope
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
t.Fatal(err)
}
if env.Error.Code != string(errcode.AgentProviderUnavailable) || env.Error.Message == "" {
t.Fatalf("body %+v", env.Error)
}
if env.Error.Reason != "模型不可达" || env.Error.TraceID != "trace-abc-1234" {
t.Fatalf("body %+v", env.Error)
}
if rec.Header().Get(TraceHeader) != "trace-abc-1234" {
t.Fatal("trace header not echoed")
}
}
func TestWriteErrorUnknownFallsBack(t *testing.T) {
reg := errcode.NewRegistry()
domains.RegisterAll(reg)
rec := httptest.NewRecorder()
WriteError(reg, rec, httptest.NewRequest(http.MethodGet, "/", nil), errcode.New("no.such_code", "x"))
if rec.Code != http.StatusInternalServerError {
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
}
var env Envelope
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
t.Fatal(err)
}
if env.Error.Code != string(errcode.SysUnknownCode) {
t.Fatalf("code %s", env.Error.Code)
}
}