fix: force Content-Type to application/json on non-streaming responses

When the gateway forces streaming to the upstream, the upstream returns
Content-Type: text/event-stream. In the non-streaming branches, the body
is correctly aggregated into JSON, but WriteFilteredHeaders has already
passed the upstream's text/event-stream header through. Since Gin's
c.JSON/c.Data only set Content-Type via writeContentType when it is not
already present, the SSE header is never overwritten -- so the body is JSON
while the header says SSE.

Downstream middlewares that decide "is this streaming?" by Content-Type
(e.g. new-api) then mis-handle the response as SSE, breaking non-streaming
JSON clients.

Fix: explicitly Set Content-Type to application/json right after
WriteFilteredHeaders in the three non-streaming JSON write paths.
This commit is contained in:
xzz0081
2026-06-07 02:56:26 +08:00
parent 635ad81cdc
commit 154e0ed6c5
3 changed files with 17 additions and 0 deletions
@@ -318,6 +318,12 @@ func (s *GatewayService) handleCCBufferedFromAnthropic(
if s.responseHeaderFilter != nil {
responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
}
// 非流式响应必须是 application/json。上游被强制流式后会返回
// Content-Type: text/event-stream,经 WriteFilteredHeaders 透传后会污染
// 响应头;而 c.Data/c.JSON 走 Gin 的 writeContentType(仅当头不存在时才设置),
// 无法覆盖已存在的 SSE 头。这里显式 Set 强制改回 JSON,避免下游中间层
// (如 new-api)按 Content-Type 误判为流式。
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
// Marshal then bytes-replace so tool name mapping is reversed at byte level
// (parity with Parrot non-stream flow that marshals → restore → emit).
if respBytes, err := json.Marshal(ccResp); err == nil {
@@ -337,6 +337,12 @@ func (s *GatewayService) handleResponsesBufferedStreamingResponse(
if s.responseHeaderFilter != nil {
responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
}
// 非流式响应必须是 application/json。上游被强制流式后会返回
// Content-Type: text/event-stream,经 WriteFilteredHeaders 透传后会污染
// 响应头;而 c.Data/c.JSON 走 Gin 的 writeContentType(仅当头不存在时才设置),
// 无法覆盖已存在的 SSE 头。这里显式 Set 强制改回 JSON,避免下游中间层
// (如 new-api)按 Content-Type 误判为流式。
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
if respBytes, err := json.Marshal(responsesResp); err == nil {
respBytes = reverseToolNamesIfPresent(c, respBytes)
c.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
@@ -407,6 +407,11 @@ func (s *OpenAIGatewayService) handleChatBufferedStreamingResponse(
if s.responseHeaderFilter != nil {
responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
}
// 非流式响应必须为标准 JSON。上游被强制流式,其响应头 Content-Type 为
// text/event-stream,会经 WriteFilteredHeaders 透传进来;而 c.JSON 走 Gin 的
// writeContentType 仅在头不存在时才设置,无法覆盖。这里显式 Set 强制改回 JSON,
// 否则下游"看头判流式"的中间层(如 new-api)会把本应聚合的 JSON 当成 SSE 处理。
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.JSON(http.StatusOK, chatResp)
return &OpenAIForwardResult{