2026-02-14 11:23:10 +08:00
package handler
import (
2026-09-26 16:59:08 +08:00
"context"
2026-02-14 11:23:10 +08:00
"encoding/json"
2026-06-09 22:46:06 +08:00
"errors"
2026-02-14 11:23:10 +08:00
"net/http"
"net/http/httptest"
2026-06-09 22:46:06 +08:00
"strings"
2026-02-14 11:23:10 +08:00
"testing"
2026-07-26 19:37:24 +09:00
"github.com/Wei-Shaw/sub2api/internal/service"
2026-02-14 11:23:10 +08:00
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGatewayEnsureForwardErrorResponse_WritesFallbackWhenNotWritten ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodGet , "/" , nil )
h := & GatewayHandler {}
wrote := h . ensureForwardErrorResponse ( c , false )
require . True ( t , wrote )
require . Equal ( t , http . StatusBadGateway , w . Code )
var parsed map [ string ] any
err := json . Unmarshal ( w . Body . Bytes (), & parsed )
require . NoError ( t , err )
assert . Equal ( t , "error" , parsed [ "type" ])
errorObj , ok := parsed [ "error" ].( map [ string ] any )
require . True ( t , ok )
assert . Equal ( t , "upstream_error" , errorObj [ "type" ])
assert . Equal ( t , "Upstream request failed" , errorObj [ "message" ])
}
2026-05-24 22:00:56 +08:00
// Writer 已写后 ensureForwardErrorResponse 必须把错误以 SSE 形式追加,
// 而不是 silent EOF。非 /responses 路径走 legacy data:{"type":"error"} 分支。
func TestGatewayEnsureForwardErrorResponse_AppendsSSEAfterWritten ( t * testing . T ) {
2026-02-14 11:23:10 +08:00
gin . SetMode ( gin . TestMode )
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodGet , "/" , nil )
c . String ( http . StatusTeapot , "already written" )
h := & GatewayHandler {}
wrote := h . ensureForwardErrorResponse ( c , false )
2026-05-24 22:00:56 +08:00
require . True ( t , wrote )
2026-02-14 11:23:10 +08:00
require . Equal ( t , http . StatusTeapot , w . Code )
2026-05-24 22:00:56 +08:00
assert . Contains ( t , w . Body . String (), "already written" )
assert . Contains ( t , w . Body . String (), `data: {"type":"error"` )
}
2026-07-26 19:37:24 +09:00
func TestGatewayEnsureForwardErrorResponse_SkipsCommittedSSEError ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointResponses , nil )
c . Header ( "Content-Type" , "text/event-stream" )
_ , _ = c . Writer . WriteString ( "event: error\ndata: {\"type\":\"error\"}\n\n" )
service . MarkResponseCommitted ( c )
h := & GatewayHandler {}
wrote := h . ensureForwardErrorResponse ( c , true )
require . False ( t , wrote )
require . Equal ( t , 1 , strings . Count ( w . Body . String (), "event: error" ))
}
2026-09-26 16:59:08 +08:00
// 客户端已断开且响应未提交:不补写 502,标记 499 供访问日志与 ops 归类。
func TestGatewayEnsureForwardErrorResponse_SkipsCanceledClient ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
ctx , cancel := context . WithCancel ( context . Background ())
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil ). WithContext ( ctx )
cancel ()
h := & GatewayHandler {}
wrote := h . ensureForwardErrorResponse ( c , false )
require . False ( t , wrote )
require . Equal ( t , statusClientClosedRequest , c . Writer . Status ())
require . Empty ( t , w . Body . String ())
}
// 客户端已断开但流已开始:状态码已固化为 200,不再向断开的连接追加错误帧。
func TestGatewayEnsureForwardErrorResponse_CanceledClientAfterStreamStartedAppendsNothing ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
ctx , cancel := context . WithCancel ( context . Background ())
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil ). WithContext ( ctx )
c . Header ( "Content-Type" , "text/event-stream" )
_ , _ = c . Writer . WriteString ( ":\n\n" )
cancel ()
h := & GatewayHandler {}
wrote := h . ensureForwardErrorResponse ( c , true )
require . False ( t , wrote )
require . Equal ( t , http . StatusOK , c . Writer . Status ())
require . Equal ( t , ":\n\n" , w . Body . String ())
require . Empty ( t , service . GetOpsStreamErrors ( c ))
}
2026-05-24 22:00:56 +08:00
// case B 回归:Anthropic-backed /responses, Writer 已被写过时
// ensureForwardErrorResponse 仍要发 response.failed。
func TestGatewayEnsureForwardErrorResponse_ResponsesRouteAfterWrittenEmitsResponseFailed ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointResponses , nil )
_ , _ = c . Writer . WriteString ( ":\n\n" )
h := & GatewayHandler {}
wrote := h . ensureForwardErrorResponse ( c , false )
require . True ( t , wrote )
body := w . Body . String ()
assert . Contains ( t , body , ":\n\n" )
assert . Contains ( t , body , "event: response.failed\n" )
assert . Contains ( t , body , `"type":"response.failed"` )
2026-02-14 11:23:10 +08:00
}
2026-06-09 22:46:06 +08:00
func TestGatewayForwardErrorAlreadyCommunicated ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
t . Run ( "json error already written" , func ( t * testing . T ) {
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil )
before := c . Writer . Size ()
c . JSON ( http . StatusBadGateway , gin . H {
"type" : "error" ,
"error" : gin . H {
"type" : "upstream_error" ,
"message" : "Your Claude Code version (2.1.39) is below the minimum required version (2.1.81). Please update: npm update -g @anthropic-ai/claude-code" ,
},
})
reported := gatewayForwardErrorAlreadyCommunicated ( c , before , errors . New ( "upstream error: 400 message=version too low" ))
require . True ( t , reported )
body := w . Body . String ()
assert . NotContains ( t , body , `data: {"type":"error"` )
})
t . Run ( "sse ping still needs fallback" , func ( t * testing . T ) {
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil )
c . Header ( "Content-Type" , "text/event-stream" )
before := c . Writer . Size ()
_ , _ = c . Writer . WriteString ( ":\n\n" )
reported := gatewayForwardErrorAlreadyCommunicated ( c , before , errors . New ( "stream read error: unexpected EOF" ))
require . False ( t , reported )
})
t . Run ( "no write still needs fallback" , func ( t * testing . T ) {
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil )
reported := gatewayForwardErrorAlreadyCommunicated ( c , c . Writer . Size (), errors . New ( "upstream request failed" ))
require . False ( t , reported )
})
// apikey 场景核心回归:复刻 GatewayService.handleErrorResponse 的 case 400 ——
// 原样透传上游 JSON body 后返回 err。此时错误已经完整告知客户端,
// handler 不得再追加 data:{"type":"error"} 帧,否则响应被污染成「JSON + 一行 data:」。
t . Run ( "upstream 400 json passthrough via c.Data" , func ( t * testing . T ) {
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil )
before := c . Writer . Size ()
upstreamBody := [] byte ( `{"type":"error","error":{"type":"upstream_error","message":"Your Claude Code version (2.1.39) is below the minimum required version (2.1.81). Please update: npm update -g @anthropic-ai/claude-code"}}` )
c . Data ( http . StatusBadRequest , "application/json" , upstreamBody )
reported := gatewayForwardErrorAlreadyCommunicated ( c , before , errors . New ( "upstream error: 400 message=version too low" ))
require . True ( t , reported )
body := w . Body . String ()
assert . NotContains ( t , body , `data: {"type":"error"` )
// 客户端只应收到上游那一份错误,没有被追加第二份。
assert . Equal ( t , 1 , strings . Count ( body , `"type":"error"` ))
})
// 流式已开始(已 flush 真实 SSE 事件,不只是 ping)+ 上游中途 400:
// HTTP 200 已固化,仍需 handler 补协议级终止帧,故不算「已完整告知」。
t . Run ( "streaming 400 mid-stream still needs fallback" , func ( t * testing . T ) {
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil )
c . Header ( "Content-Type" , "text/event-stream" )
before := c . Writer . Size ()
_ , _ = c . Writer . WriteString ( "event: message_start\ndata: {\"type\":\"message_start\"}\n\n" )
reported := gatewayForwardErrorAlreadyCommunicated ( c , before , errors . New ( "upstream error: 400 message=version too low" ))
require . False ( t , reported )
})
// 防御边界:err 为 nil 时永远不算「已告知」,避免在成功路径误吞兜底逻辑。
t . Run ( "nil error never reports communicated" , func ( t * testing . T ) {
w := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( w )
c . Request = httptest . NewRequest ( http . MethodPost , EndpointMessages , nil )
c . JSON ( http . StatusOK , gin . H { "ok" : true })
reported := gatewayForwardErrorAlreadyCommunicated ( c , 0 , nil )
require . False ( t , reported )
})
}