feat(gateway): collapse multi-tier failure details into short client message

Clients saw the full per-tier chain error including upstream HTML WAF
pages and JSON quota payloads. The response now carries one capped
one-line reason per tier (quota/cooling skips preserved); full detail
remains in rec.Err / stats API and is logged server-side.
This commit is contained in:
JianFeeeee
2026-08-24 16:02:31 +08:00
parent e19cff248f
commit 31b1ed2c96
2 changed files with 41 additions and 6 deletions

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strings"
"sync/atomic"
@ -523,6 +524,40 @@ func upstreamErrStatus(err error) int {
return http.StatusBadGateway
}
// clientUpstreamErr collapses upstream failure details into a short message
// for the client: per-tier bodies (WAF HTML pages, quota payloads, ...) stay
// in rec.Err / the stats API and the server log instead of the response.
// Per-tier one-line reasons are kept (quota/cooling skips carry no error body
// and are the actionable part); each is capped so HTML dumps can't leak.
func clientUpstreamErr(err error) string {
log.Printf("[gateway] upstream failure surfaced to client: %v", err)
var ce *scheduler.ChainErr
if errors.As(err, &ce) {
parts := make([]string, 0, len(ce.Tiers)+len(ce.Skipped))
for _, t := range ce.Tiers {
parts = append(parts, oneLine(fmt.Sprintf("%s/%s: %v", t.Source, t.Model, t.Err), 80))
}
for _, sk := range ce.Skipped {
parts = append(parts, oneLine(sk, 80))
}
msg := strings.Join(parts, "; ")
if len(msg) > 300 {
msg = msg[:300] + "..."
}
return fmt.Sprintf("all %d auto providers failed: %s", len(parts), msg)
}
return oneLine(err.Error(), 160)
}
// oneLine flattens an error string to a single line capped at n chars.
func oneLine(s string, n int) string {
s = strings.Join(strings.Fields(s), " ")
if len(s) > n {
s = s[:n] + "..."
}
return s
}
func (g *Gateway) singleChat(w http.ResponseWriter, ctx context.Context, cands []*provider.Provider, req *types.ChatRequest, effective string, rec *Req) {
rec.LatMs = 0
t0 := time.Now()
@ -533,7 +568,7 @@ func (g *Gateway) singleChat(w http.ResponseWriter, ctx context.Context, cands [
rec.Status = upstreamErrStatus(err)
rec.Err = err.Error()
g.writeRec(rec)
writeError(w, rec.Status, "upstream_error", err.Error())
writeError(w, rec.Status, "upstream_error", clientUpstreamErr(err))
return
}
rec.OK = true
@ -620,7 +655,7 @@ func (g *Gateway) streamChat(w http.ResponseWriter, ctx context.Context, cands [
rec.OK = false
rec.Status = upstreamErrStatus(err)
rec.Err = err.Error()
writeError(w, rec.Status, "upstream_error", err.Error())
writeError(w, rec.Status, "upstream_error", clientUpstreamErr(err))
return
}
if usedModel != "" {
@ -741,7 +776,7 @@ func (g *Gateway) singleChatAuto(w http.ResponseWriter, ctx context.Context, cha
rec.Model = ce.Tiers[0].Model
}
g.writeRec(rec)
writeError(w, rec.Status, "upstream_error", err.Error())
writeError(w, rec.Status, "upstream_error", clientUpstreamErr(err))
return
}
rec.OK = true
@ -799,7 +834,7 @@ func (g *Gateway) streamChatAuto(w http.ResponseWriter, ctx context.Context, cha
rec.Source = ce.Tiers[0].Source
rec.Model = ce.Tiers[0].Model
}
writeError(w, rec.Status, "upstream_error", err.Error())
writeError(w, rec.Status, "upstream_error", clientUpstreamErr(err))
return
}
if usedModel != "" {
@ -946,7 +981,7 @@ func (g *Gateway) handleImage(w http.ResponseWriter, r *http.Request) {
rec.Status = upstreamErrStatus(err)
rec.Err = err.Error()
g.writeRec(rec)
writeError(w, rec.Status, "upstream_error", err.Error())
writeError(w, rec.Status, "upstream_error", clientUpstreamErr(err))
return
}
if usedSrc != "" {

View File

@ -139,7 +139,7 @@ func TestChatAutoChain503Summary(t *testing.T) {
if rr.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d body=%s", rr.Code, rr.Body.String())
}
if !strings.Contains(rr.Body.String(), "all auto tiers failed") ||
if !strings.Contains(rr.Body.String(), "auto providers failed") ||
!strings.Contains(rr.Body.String(), "a/a-m") ||
!strings.Contains(rr.Body.String(), "b/b-m") {
t.Fatalf("503 must summarize every tier, body=%s", rr.Body.String())