flipped-aurora/gin-vue-admin · error

上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w

Error message

上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w

What it means

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.

Source

Thrown at server/api/v1/system/sys_auto_code.go:174

		return true
	}
	if stream, ok := llm["stream"].(bool); ok && stream {
		return true
	}
	return strings.Contains(strings.ToLower(c.GetHeader("Accept")), "text/event-stream")
}

func (autoApi *AutoCodeApi) proxyLLMStream(c *gin.Context, llm common.JSONMap) error {
	res, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode < 200 || res.StatusCode >= 300 {
		body, readErr := io.ReadAll(res.Body)
		if readErr != nil {
			return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w", res.StatusCode, res.Header.Get("Content-Type"), readErr)
		}
		return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, res.Header.Get("Content-Type"), previewResponseBody(body))
	}

	flusher, ok := c.Writer.(http.Flusher)
	if !ok {
		return errors.New("当前响应不支持流式输出")
	}

	copyLLMStreamHeaders(c.Writer.Header(), res.Header)
	if c.Writer.Header().Get("Content-Type") == "" {
		c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
	}
	if c.Writer.Header().Get("Cache-Control") == "" {
		c.Writer.Header().Set("Cache-Control", "no-cache")
	}
	c.Writer.Header().Set("Connection", "keep-alive")
	c.Writer.Header().Set("X-Accel-Buffering", "no")

View on GitHub (pinned to 3136500ef3)

Solutions

  1. Log and inspect the wrapped read-body-err to find why the body was unreadable (connection reset vs already-closed body).
  2. 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.
  3. Retry the request once; transient resets on error responses are common behind proxies.
  4. 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.

Example fix

// before
body, readErr := io.ReadAll(res.Body)
if readErr != nil {
    return fmt.Errorf("...read-body-err=%w", res.StatusCode, ct, readErr)
}

// after
body, readErr := io.ReadAll(io.LimitReader(res.Body, 4096))
if readErr != nil {
    return fmt.Errorf("upstream non-2xx: status=%d content-type=%s (body unreadable)", res.StatusCode, ct)
}
Defensive patterns

Strategy: retry

Validate before calling

req, _ := http.NewRequestWithContext(ctx, http.MethodPost, llmURL, body)
resp, err := client.Do(req)
if err != nil { return err }
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    return fmt.Errorf("LLM endpoint unhealthy: status=%d", resp.StatusCode)
}

Try / catch

err := api.proxyLLMStream(c, payload)
if err != nil {
    var upErr *url.Error
    if errors.As(err, &upErr) || strings.Contains(err.Error(), "read-body-err") {
        // transient upstream failure: retry once or return 502
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of flipped-aurora/gin-vue-admin@3136500ef3 (2026-08-31). Data as JSON: /api/errors/e4ea4d007342ad4e. Report an issue: GitHub.