package httpx import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "log/slog" "net/http" "strings" ) const ( CodeAUTHZ = "AUTHZ" CodeVALIDATION = "VALIDATION" CodeVERSION_CONFLICT = "VERSION_CONFLICT" CodeIDEMPOTENCY_CONFLICT = "IDEMPOTENCY_CONFLICT" CodeINTERNAL = "INTERNAL" HeaderRequestID = "X-Request-ID" HeaderErrorCode = "X-Error-Code" ) type Envelope struct { Code string `json:"code"` Message string `json:"message"` RequestID string `json:"request_id"` Error string `json:"error"` Details map[string]any `json:"details,omitempty"` } func CodeFromStatus(status int) string { switch status { case http.StatusUnauthorized, http.StatusForbidden: return CodeAUTHZ case http.StatusConflict: return CodeVERSION_CONFLICT case http.StatusBadRequest, http.StatusUnprocessableEntity: return CodeVALIDATION default: if status >= 500 { return CodeINTERNAL } if status == http.StatusNotFound { return CodeAUTHZ } return CodeVALIDATION } } func SafeMessage(code string) string { switch code { case CodeAUTHZ: return "无权访问" case CodeVALIDATION: return "请求参数无效" case CodeVERSION_CONFLICT: return "资源版本冲突" case CodeIDEMPOTENCY_CONFLICT: return "重复提交" default: return "请求无法完成" } } func LooksInternal(s string) bool { low := strings.ToLower(s) needles := []string{"sql", "pq:", "pgx", "postgres", "stack", "panic", "token", "password", "secret", "api_key", "bearer ", "../", "prompt", "commit", "rollback", "transaction", "constraint", "violates"} for _, n := range needles { if strings.Contains(low, n) { return true } } if strings.Contains(s, "/home/") || strings.Contains(s, "/var/") || strings.Contains(s, "\\") { return true } return false } func RequestID(r *http.Request) string { if r == nil { return newRequestID() } if v := strings.TrimSpace(r.Header.Get(HeaderRequestID)); v != "" { return v } return newRequestID() } func newRequestID() string { var b [8]byte _, _ = rand.Read(b[:]) return "req_" + hex.EncodeToString(b[:]) } func PublicLegacy(code, legacy string) string { legacy = strings.TrimSpace(legacy) if legacy == "" || LooksInternal(legacy) { return code } if strings.HasPrefix(legacy, "<") || strings.Contains(legacy, "= 500 { level = slog.LevelError } args := []any{"code", code, "status", status, "request_id", requestID} if r != nil { args = append(args, "method", r.Method, "path", stripQuery(r.URL.Path)) } slog.Log(context.Background(), level, "http_error", args...) } func stripQuery(path string) string { path = strings.TrimSpace(path) if path == "" { return "/" } if i := strings.IndexAny(path, "?#"); i >= 0 { path = path[:i] } return path }