mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 17:07:59 +00:00
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:
@ -434,7 +434,7 @@ func (p *Provider) probeModels(ctx context.Context) (bool, string) {
|
|||||||
case resp.StatusCode == 404 || resp.StatusCode == 405:
|
case resp.StatusCode == 404 || resp.StatusCode == 405:
|
||||||
return false, ""
|
return false, ""
|
||||||
default:
|
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 {
|
} else if status == 200 {
|
||||||
ok = true
|
ok = true
|
||||||
} else {
|
} else {
|
||||||
msg = fmt.Sprintf("api error %d: %s", status, truncate(raw, 500))
|
msg = shortAPIError(status, raw)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
msg = err.Error()
|
msg = err.Error()
|
||||||
@ -717,7 +717,7 @@ func (p *Provider) Chat(ctx context.Context, req *types.ChatRequest) (*types.Uni
|
|||||||
}
|
}
|
||||||
if status != 200 {
|
if status != 200 {
|
||||||
p.ReportStatus(model, status)
|
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)
|
unified, err := p.vm.Transform(p.adapter, "transform_response", raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -793,7 +793,7 @@ func (p *Provider) ChatStream(ctx context.Context, req *types.ChatRequest) (<-ch
|
|||||||
sel.resp.Body.Close()
|
sel.resp.Body.Close()
|
||||||
p.ReportStatus(model, sel.resp.StatusCode)
|
p.ReportStatus(model, sel.resp.StatusCode)
|
||||||
p.Release()
|
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() {
|
go func() {
|
||||||
defer p.Release()
|
defer p.Release()
|
||||||
@ -928,6 +928,56 @@ func errorOnlyChunk(ck types.UnifiedChunk) bool {
|
|||||||
ck.ReasoningContent == "" && ck.Usage == nil
|
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
|
// Image generates images via /v1/images/generations. Same scheduling-state
|
||||||
// accounting as Chat: fail fast on busy, record per (source, model).
|
// accounting as Chat: fail fast on busy, record per (source, model).
|
||||||
func (p *Provider) Image(ctx context.Context, req *types.ImageGenRequest) (*types.UnifiedResponse, error) {
|
func (p *Provider) Image(ctx context.Context, req *types.ImageGenRequest) (*types.UnifiedResponse, error) {
|
||||||
|
|||||||
@ -538,3 +538,30 @@ func TestStandardSSEChunkFinishReason(t *testing.T) {
|
|||||||
t.Fatalf("empty-string finish reason is not a finish signal: %s", out)
|
t.Fatalf("empty-string finish reason is not a finish signal: %s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShortAPIError(t *testing.T) {
|
||||||
|
cases := []struct{ name, body, want string }{
|
||||||
|
{"openai-style envelope", `{"error":{"message":"Allocated quota exceeded","type":"invalid_request_error","code":"insufficient_quota"}}`, "api error 429: Allocated quota exceeded"},
|
||||||
|
{"nested console envelope", `{"error":{"type":"server_error","message":"Error from provider (Console): Upstream request failed: Endpoint is unavailable."}}`, "api error 503: Error from provider (Console): Upstream request failed: Endpoint is unavailable."},
|
||||||
|
{"string error", `{"error":"boom"}`, "api error 500: boom"},
|
||||||
|
{"html waf page", "<!doctypehtml><html lang=\"zh-cn\"><title>405</title></html>", "api error 405: upstream returned an HTML error page"},
|
||||||
|
{"plain text body", "service unavailable", "api error 503: service unavailable"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := shortAPIError(statusFor(c.want), c.body); got != c.want {
|
||||||
|
t.Fatalf("%s: got %q want %q", c.name, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusFor(want string) int {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(want, "429"):
|
||||||
|
return 429
|
||||||
|
case strings.Contains(want, "503"):
|
||||||
|
return 503
|
||||||
|
case strings.Contains(want, "405"):
|
||||||
|
return 405
|
||||||
|
}
|
||||||
|
return 500
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user