{"record":{"id":"c24299ce3197888f","repo":"flipped-aurora/gin-vue-admin","slug":"2xx-status-d-content-type-s-read-b-c24299","errorCode":null,"errorMessage":"上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w","messagePattern":"上游大模型流式服务返回非 2xx: status=(.+?) content-type=(.+?) read-body-err=%w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/api/v1/system/sys_auto_code_sse.go","lineNumber":61,"sourceCode":"\t\tif c.Writer.Written() {\n\t\t\twriteLLMStreamError(c, err)\n\t\t\treturn\n\t\t}\n\t\tresponse.FailWithMessage(err.Error(), c)\n\t}\n}\n\nfunc (autoApi *AutoCodeApi) streamLLMAsSSE(c *gin.Context, llm common.JSONMap) error {\n\tres, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"调用上游大模型失败: %w\", err)\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode < 200 || res.StatusCode >= 300 {\n\t\tbody, readErr := io.ReadAll(res.Body)\n\t\tif readErr != nil {\n\t\t\treturn fmt.Errorf(\"上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w\", res.StatusCode, res.Header.Get(\"Content-Type\"), readErr)\n\t\t}\n\t\treturn fmt.Errorf(\"上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s\", res.StatusCode, res.Header.Get(\"Content-Type\"), previewResponseBody(body))\n\t}\n\n\tct := res.Header.Get(\"Content-Type\")\n\tlogger.WithCtx(c.Request.Context()).Mod(\"biz\").Field(\"status\", res.StatusCode).Field(\"content-type\", ct).Info(\"LLMAutoSSE 上游返回成功，开始 SSE 流式转发\")\n\n\t// 如果上游返回的不是 SSE 流（可能是 blocking 模式返回的 JSON），直接读取并转发\n\tif !strings.Contains(ct, \"text/event-stream\") && !strings.Contains(ct, \"text/plain\") {\n\t\tbody, readErr := io.ReadAll(res.Body)\n\t\tif readErr != nil {\n\t\t\treturn fmt.Errorf(\"读取上游非流式响应失败: %w\", readErr)\n\t\t}\n\t\tlogger.WithCtx(c.Request.Context()).Mod(\"biz\").Field(\"body_preview\", previewResponseBody(body)).Warn(\"LLMAutoSSE 上游返回非 SSE 流，Content-Type: \"+ct+\", 将以单次事件转发\")\n\n\t\tflusher, ok := c.Writer.(http.Flusher)\n\t\tif !ok {\n\t\t\treturn errors.New(\"当前响应不支持流式输出\")","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/flipped-aurora/gin-vue-admin/blob/3136500ef380842b0eb6c4daa87c3f8a47fcf9e0/server/api/v1/system/sys_auto_code_sse.go#L43-L79","documentation":"The SSE variant of the non-2xx check: streamLLMAsSSE (called by LLMAutoSSE) inspects res.StatusCode after LLMAutoStream returns, and when reading the error body of a non-2xx response itself fails, returns the same '上游大模型流式服务返回非 2xx' message carrying status, Content-Type, and the body-read error via %w. It exists so SSE clients still receive a structured upstream failure even when the error payload is unreadable.","triggerScenarios":"LLMAutoSSE -> streamLLMAsSSE receives a 4xx/5xx from the upstream LLM whose body cannot be read: the upstream closed the chunked connection on error, the body was aborted mid-transfer, or the connection reset before any error bytes arrived.","commonSituations":"Expired API key causing the provider to reset the error response, upstream CDN/WAF closing connections on rejection, quota-exceeded 429 with aborted body, provider outage producing truncated 5xx responses.","solutions":["Inspect the wrapped read-body-err; 'unexpected EOF'/'connection reset' means the upstream closed the error response early.","Verify API key, quota, and model configuration — non-2xx under SSE almost always traces to auth or rate limiting.","Retry with backoff for transient 429/5xx; surface a server-sent error event to the SSE client so the UI can react.","Capture upstream logs/status page to confirm whether the provider is dropping error responses during an incident."],"exampleFix":"// before\n// error swallowed as read failure, no context to debug\nreturn fmt.Errorf(\"...read-body-err=%w\", status, ct, readErr)\n\n// after\nlogger.WithCtx(c.Request.Context()).Mod(\"biz\").Field(\"status\", status).Warn(\"上游返回非2xx且body不可读\")\nreturn fmt.Errorf(\"upstream non-2xx: status=%d (error body unreadable: %v)\", status, readErr)","handlingStrategy":"fallback","validationCode":"resp, err := svc.LLMAutoStream(ctx, llm)\nif err != nil { return err }\nif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n    return fmt.Errorf(\"LLM upstream %d\", resp.StatusCode) // decide SSE error event now\n}","typeGuard":null,"tryCatchPattern":"if err := api.streamLLMAsSSE(c, payload); err != nil {\n    c.SSEvent(\"error\", gin.H{\"msg\": \"upstream LLM rejected the request\"})\n    c.Writer.Flush()\n    return nil // keep the SSE channel valid for the client\n}","preventionTips":["Send SSE 'error' events instead of aborting so clients get structured failures","Pre-check API key/quota before opening streams","Treat unreadable error bodies as auth/quota signals and alert on them","Cap error-body reads with io.LimitReader and report status even when the body fails"],"tags":["network","http","sse","llm","gin-vue-admin"],"backgroundTag":"upstream-http-error","analyzedSha":"3136500ef380842b0eb6c4daa87c3f8a47fcf9e0","analyzedAt":"2026-08-31T13:50:02.721Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}