flipped-aurora/gin-vue-admin · error

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

Error message

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

What it means

streamLLMAsSSE proxies a streaming LLM upstream to the client as SSE. When the upstream HTTP response status is outside 200-299, it treats the proxy as failed and returns this error including the upstream status, Content-Type and a truncated body preview so the caller can surface what the upstream rejected.

Source

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

			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("当前响应不支持流式输出")
		}
		prepareSSEHeaders(c)

View on GitHub (pinned to 3136500ef3)

Solutions

  1. Read the status and body preview in the error to identify the upstream rejection cause
  2. Verify the upstream API key/token configured for the LLM service is valid and not expired
  3. Check request payload (model name, params) matches the upstream API contract
  4. If 429, reduce request rate or wait and retry; if 5xx, check upstream service health

Example fix

// before
res, _ := http.DefaultClient.Do(req)
// after
res, err := http.DefaultClient.Do(req)
if err != nil { return err }
if res.StatusCode < 200 || res.StatusCode >= 300 {
    b, _ := io.ReadAll(res.Body)
    return fmt.Errorf("upstream %d: %s", res.StatusCode, string(b))
}
Defensive patterns

Strategy: validation

Validate before calling

if res.StatusCode < 200 || res.StatusCode >= 300 {
    b, _ := io.ReadAll(res.Body)
    return fmt.Errorf("upstream %d: %s", res.StatusCode, string(b))
}

Try / catch

if err := streamLLMAsSSE(c, ...); err != nil {
    var statusErr *UpstreamStatusError
    if errors.As(err, &statusErr) { /* inspect statusErr.Code/Body */ }
}

Prevention

When it happens

Trigger: The HTTP request to the LLM upstream completes but res.StatusCode < 200 or >= 300, e.g. upstream returns 401 for a bad/expired API key, 429 for rate limiting, or 500 from the model provider.

Common situations: Wrong or expired LLM API key in config; upstream service down or overloaded (5xx); request payload rejected (400 invalid model/params); quota exhausted (429).

Related errors


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