Files
Agent-System/internal/httpx/envelope.go
weichenandCursor f6813cc272 feat(api): add additive HTTP/SSE error envelopes
Public errors now emit code, safe message, and request_id while keeping the legacy error field. Raw SQL, paths, tokens, and XSS payloads are stripped; the frontend branches on code instead of substring matching.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 14:40:56 +00:00

124 lines
2.8 KiB
Go

package httpx
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strings"
)
const (
CodeAUTHZ = "AUTHZ"
CodeVALIDATION = "VALIDATION"
CodeVERSION_CONFLICT = "VERSION_CONFLICT"
CodeIDEMPOTENCY_CONFLICT = "IDEMPOTENCY_CONFLICT"
CodeINTERNAL = "INTERNAL"
HeaderRequestID = "X-Request-ID"
)
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:", "postgres", "stack", "panic", "token", "password", "secret", "api_key", "bearer ", "../", "prompt"}
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, "<script") {
return code
}
return legacy
}
func Write(w http.ResponseWriter, status int, code, legacy, requestID string, details map[string]any) {
if code == "" {
code = CodeFromStatus(status)
}
if requestID == "" {
requestID = newRequestID()
}
env := Envelope{
Code: code,
Message: SafeMessage(code),
RequestID: requestID,
Error: PublicLegacy(code, legacy),
Details: details,
}
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
}
w.Header().Set(HeaderRequestID, requestID)
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(env)
}