fix(provider): condense upstream error bodies before they reach clients

api error strings embedded raw upstream response bodies, so JSON quota
payloads and WAF HTML pages leaked through to clients (and through the
per-tier chain summary). shortAPIError extracts the envelope reason
(error.message / message / msg), collapses HTML blocklist pages to a
marker, and caps everything at one line.
This commit is contained in:
JianFeeeee
2026-08-24 18:56:06 +08:00
parent e3a36bc7ca
commit 6eac80bc6c
2 changed files with 81 additions and 4 deletions

View File

@ -434,7 +434,7 @@ func (p *Provider) probeModels(ctx context.Context) (bool, string) {
case resp.StatusCode == 404 || resp.StatusCode == 405:
return false, ""
default:
return false, fmt.Sprintf("api error %d: %s", resp.StatusCode, truncate(string(raw), 300))
return false, shortAPIError(resp.StatusCode, string(raw))
}
}
@ -471,7 +471,7 @@ func (p *Provider) probeChat(ctx context.Context) (bool, string) {
} else if status == 200 {
ok = true
} else {
msg = fmt.Sprintf("api error %d: %s", status, truncate(raw, 500))
msg = shortAPIError(status, raw)
}
} else {
msg = err.Error()
@ -717,7 +717,7 @@ func (p *Provider) Chat(ctx context.Context, req *types.ChatRequest) (*types.Uni
}
if status != 200 {
p.ReportStatus(model, status)
return nil, fmt.Errorf("api error %d: %s", status, truncate(raw, 500))
return nil, fmt.Errorf("%s", shortAPIError(status, raw))
}
unified, err := p.vm.Transform(p.adapter, "transform_response", raw)
if err != nil {
@ -793,7 +793,7 @@ func (p *Provider) ChatStream(ctx context.Context, req *types.ChatRequest) (<-ch
sel.resp.Body.Close()
p.ReportStatus(model, sel.resp.StatusCode)
p.Release()
return nil, fmt.Errorf("api error %d: %s", sel.resp.StatusCode, truncate(string(raw), 500))
return nil, fmt.Errorf("%s", shortAPIError(sel.resp.StatusCode, string(raw)))
}
go func() {
defer p.Release()
@ -928,6 +928,56 @@ func errorOnlyChunk(ck types.UnifiedChunk) bool {
ck.ReasoningContent == "" && ck.Usage == nil
}
// shortAPIError condenses an upstream error response into its human reason:
// JSON envelopes contribute their error/message field, HTML pages (WAF
// blocklists) collapse to a marker, anything else is capped as-is. Raw
// bodies must not leak through errors to clients.
func shortAPIError(status int, body string) string {
b := strings.TrimSpace(body)
low := strings.ToLower(b)
if strings.HasPrefix(low, "<!doctype") || strings.Contains(low, "<html") {
return fmt.Sprintf("api error %d: upstream returned an HTML error page", status)
}
var env struct {
Error json.RawMessage `json:"error"`
Message string `json:"message"`
Msg string `json:"msg"`
}
if err := json.Unmarshal([]byte(b), &env); err == nil {
msg := ""
switch {
case len(env.Error) > 0:
var es string
if json.Unmarshal(env.Error, &es) == nil {
msg = es
} else {
var obj struct {
Message string `json:"message"`
}
if json.Unmarshal(env.Error, &obj) == nil {
msg = obj.Message
}
}
case env.Message != "":
msg = env.Message
case env.Msg != "":
msg = env.Msg
}
if msg != "" {
return fmt.Sprintf("api error %d: %s", status, oneLineStr(msg, 160))
}
}
return fmt.Sprintf("api error %d: %s", status, oneLineStr(b, 160))
}
func oneLineStr(s string, n int) string {
s = strings.Join(strings.Fields(s), " ")
if len(s) > n {
s = s[:n] + "..."
}
return s
}
// Image generates images via /v1/images/generations. Same scheduling-state
// accounting as Chat: fail fast on busy, record per (source, model).
func (p *Provider) Image(ctx context.Context, req *types.ImageGenRequest) (*types.UnifiedResponse, error) {