{"record":{"id":"e4ea4d007342ad4e","repo":"flipped-aurora/gin-vue-admin","slug":"2xx-status-d-content-type-s-read-b","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.go","lineNumber":174,"sourceCode":"\t\treturn true\n\t}\n\tif stream, ok := llm[\"stream\"].(bool); ok && stream {\n\t\treturn true\n\t}\n\treturn strings.Contains(strings.ToLower(c.GetHeader(\"Accept\")), \"text/event-stream\")\n}\n\nfunc (autoApi *AutoCodeApi) proxyLLMStream(c *gin.Context, llm common.JSONMap) error {\n\tres, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm)\n\tif err != nil {\n\t\treturn 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\tflusher, ok := c.Writer.(http.Flusher)\n\tif !ok {\n\t\treturn errors.New(\"当前响应不支持流式输出\")\n\t}\n\n\tcopyLLMStreamHeaders(c.Writer.Header(), res.Header)\n\tif c.Writer.Header().Get(\"Content-Type\") == \"\" {\n\t\tc.Writer.Header().Set(\"Content-Type\", \"text/event-stream; charset=utf-8\")\n\t}\n\tif c.Writer.Header().Get(\"Cache-Control\") == \"\" {\n\t\tc.Writer.Header().Set(\"Cache-Control\", \"no-cache\")\n\t}\n\tc.Writer.Header().Set(\"Connection\", \"keep-alive\")\n\tc.Writer.Header().Set(\"X-Accel-Buffering\", \"no\")","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/flipped-aurora/gin-vue-admin/blob/3136500ef380842b0eb6c4daa87c3f8a47fcf9e0/server/api/v1/system/sys_auto_code.go#L156-L192","documentation":"proxyLLMStream in server/api/v1/system/sys_auto_code.go forwards an SSE/HTTP stream from the upstream LLM service to the client. After issuing the request it checks res.StatusCode; anything outside 200-299 is a failure. If reading the error body also fails (io.ReadAll returns readErr), it returns an error wrapping the status code, the upstream Content-Type, and the body-read error via %w, so the true network/IO cause is preserved in the chain.","triggerScenarios":"Calling LLMAuto which reaches proxyLLMStream, and the upstream LLM HTTP call returns 4xx/5xx (bad API key, rate limit, upstream outage) AND the response body cannot be read — e.g. the connection was reset mid-body, the body was already consumed/closed, or a chunked stream aborted before any bytes arrived.","commonSituations":"Expired or wrong LLM_API_KEY configured so upstream returns 401 and closes the stream abruptly; upstream proxy/CDN kills the connection on error; misconfigured LLM base URL pointing at a service that hangs up; upstream returns 429 and closes the chunked body.","solutions":["Log and inspect the wrapped read-body-err to find why the body was unreadable (connection reset vs already-closed body).","Check the LLM service configuration (base URL, API key) against the upstream provider docs; a 401/403 body is often dropped because the stream aborts.","Retry the request once; transient resets on error responses are common behind proxies.","If you control the code, treat non-2xx with unreadable body the same as the readable case: report only status + content-type instead of failing on the read error."],"exampleFix":"// before\nbody, readErr := io.ReadAll(res.Body)\nif readErr != nil {\n    return fmt.Errorf(\"...read-body-err=%w\", res.StatusCode, ct, readErr)\n}\n\n// after\nbody, readErr := io.ReadAll(io.LimitReader(res.Body, 4096))\nif readErr != nil {\n    return fmt.Errorf(\"upstream non-2xx: status=%d content-type=%s (body unreadable)\", res.StatusCode, ct)\n}","handlingStrategy":"retry","validationCode":"req, _ := http.NewRequestWithContext(ctx, http.MethodPost, llmURL, body)\nresp, err := client.Do(req)\nif err != nil { return err }\nif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n    return fmt.Errorf(\"LLM endpoint unhealthy: status=%d\", resp.StatusCode)\n}","typeGuard":null,"tryCatchPattern":"err := api.proxyLLMStream(c, payload)\nif err != nil {\n    var upErr *url.Error\n    if errors.As(err, &upErr) || strings.Contains(err.Error(), \"read-body-err\") {\n        // transient upstream failure: retry once or return 502\n    }\n    return err\n}","preventionTips":["Health-check the LLM endpoint at startup and on interval","Use io.LimitReader when reading error bodies so truncation can't fail the report","Retry transient non-2xx statuses (429/5xx) with backoff","Log status+content-type even when the body is unreadable"],"tags":["network","http","streaming","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"}