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

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.

Source

Thrown at server/api/v1/system/sys_auto_code_sse.go:61

		if c.Writer.Written() {
			writeLLMStreamError(c, err)
			return
		}
		response.FailWithMessage(err.Error(), c)
	}
}

func (autoApi *AutoCodeApi) streamLLMAsSSE(c *gin.Context, llm common.JSONMap) error {
	res, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm)
	if err != nil {
		return fmt.Errorf("调用上游大模型失败: %w", 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))
	}

	ct := res.Header.Get("Content-Type")
	logger.WithCtx(c.Request.Context()).Mod("biz").Field("status", res.StatusCode).Field("content-type", ct).Info("LLMAutoSSE 上游返回成功,开始 SSE 流式转发")

	// 如果上游返回的不是 SSE 流(可能是 blocking 模式返回的 JSON),直接读取并转发
	if !strings.Contains(ct, "text/event-stream") && !strings.Contains(ct, "text/plain") {
		body, readErr := io.ReadAll(res.Body)
		if readErr != nil {
			return fmt.Errorf("读取上游非流式响应失败: %w", readErr)
		}
		logger.WithCtx(c.Request.Context()).Mod("biz").Field("body_preview", previewResponseBody(body)).Warn("LLMAutoSSE 上游返回非 SSE 流,Content-Type: "+ct+", 将以单次事件转发")

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

View on GitHub (pinned to 3136500ef3)

Solutions

  1. Inspect the wrapped read-body-err; 'unexpected EOF'/'connection reset' means the upstream closed the error response early.
  2. Verify API key, quota, and model configuration — non-2xx under SSE almost always traces to auth or rate limiting.
  3. Retry with backoff for transient 429/5xx; surface a server-sent error event to the SSE client so the UI can react.
  4. Capture upstream logs/status page to confirm whether the provider is dropping error responses during an incident.

Example fix

// before
// error swallowed as read failure, no context to debug
return fmt.Errorf("...read-body-err=%w", status, ct, readErr)

// after
logger.WithCtx(c.Request.Context()).Mod("biz").Field("status", status).Warn("上游返回非2xx且body不可读")
return fmt.Errorf("upstream non-2xx: status=%d (error body unreadable: %v)", status, readErr)
Defensive patterns

Strategy: fallback

Validate before calling

resp, err := svc.LLMAutoStream(ctx, llm)
if err != nil { return err }
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    return fmt.Errorf("LLM upstream %d", resp.StatusCode) // decide SSE error event now
}

Try / catch

if err := api.streamLLMAsSSE(c, payload); err != nil {
    c.SSEvent("error", gin.H{"msg": "upstream LLM rejected the request"})
    c.Writer.Flush()
    return nil // keep the SSE channel valid for the client
}

Prevention

When it happens

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

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

Related errors


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