gateway_handler_error_fallback_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. package handler
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. )
  14. func TestGatewayEnsureForwardErrorResponse_WritesFallbackWhenNotWritten(t *testing.T) {
  15. gin.SetMode(gin.TestMode)
  16. w := httptest.NewRecorder()
  17. c, _ := gin.CreateTestContext(w)
  18. c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
  19. h := &GatewayHandler{}
  20. wrote := h.ensureForwardErrorResponse(c, false)
  21. require.True(t, wrote)
  22. require.Equal(t, http.StatusBadGateway, w.Code)
  23. var parsed map[string]any
  24. err := json.Unmarshal(w.Body.Bytes(), &parsed)
  25. require.NoError(t, err)
  26. assert.Equal(t, "error", parsed["type"])
  27. errorObj, ok := parsed["error"].(map[string]any)
  28. require.True(t, ok)
  29. assert.Equal(t, "upstream_error", errorObj["type"])
  30. assert.Equal(t, "Upstream request failed", errorObj["message"])
  31. }
  32. // Writer 已写后 ensureForwardErrorResponse 必须把错误以 SSE 形式追加,
  33. // 而不是 silent EOF。非 /responses 路径走 legacy data:{"type":"error"} 分支。
  34. func TestGatewayEnsureForwardErrorResponse_AppendsSSEAfterWritten(t *testing.T) {
  35. gin.SetMode(gin.TestMode)
  36. w := httptest.NewRecorder()
  37. c, _ := gin.CreateTestContext(w)
  38. c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
  39. c.String(http.StatusTeapot, "already written")
  40. h := &GatewayHandler{}
  41. wrote := h.ensureForwardErrorResponse(c, false)
  42. require.True(t, wrote)
  43. require.Equal(t, http.StatusTeapot, w.Code)
  44. assert.Contains(t, w.Body.String(), "already written")
  45. assert.Contains(t, w.Body.String(), `data: {"type":"error"`)
  46. }
  47. func TestGatewayEnsureForwardErrorResponse_SkipsCommittedSSEError(t *testing.T) {
  48. gin.SetMode(gin.TestMode)
  49. w := httptest.NewRecorder()
  50. c, _ := gin.CreateTestContext(w)
  51. c.Request = httptest.NewRequest(http.MethodPost, EndpointResponses, nil)
  52. c.Header("Content-Type", "text/event-stream")
  53. _, _ = c.Writer.WriteString("event: error\ndata: {\"type\":\"error\"}\n\n")
  54. service.MarkResponseCommitted(c)
  55. h := &GatewayHandler{}
  56. wrote := h.ensureForwardErrorResponse(c, true)
  57. require.False(t, wrote)
  58. require.Equal(t, 1, strings.Count(w.Body.String(), "event: error"))
  59. }
  60. // case B 回归:Anthropic-backed /responses,Writer 已被写过时
  61. // ensureForwardErrorResponse 仍要发 response.failed。
  62. func TestGatewayEnsureForwardErrorResponse_ResponsesRouteAfterWrittenEmitsResponseFailed(t *testing.T) {
  63. gin.SetMode(gin.TestMode)
  64. w := httptest.NewRecorder()
  65. c, _ := gin.CreateTestContext(w)
  66. c.Request = httptest.NewRequest(http.MethodPost, EndpointResponses, nil)
  67. _, _ = c.Writer.WriteString(":\n\n")
  68. h := &GatewayHandler{}
  69. wrote := h.ensureForwardErrorResponse(c, false)
  70. require.True(t, wrote)
  71. body := w.Body.String()
  72. assert.Contains(t, body, ":\n\n")
  73. assert.Contains(t, body, "event: response.failed\n")
  74. assert.Contains(t, body, `"type":"response.failed"`)
  75. }
  76. func TestGatewayForwardErrorAlreadyCommunicated(t *testing.T) {
  77. gin.SetMode(gin.TestMode)
  78. t.Run("json error already written", func(t *testing.T) {
  79. w := httptest.NewRecorder()
  80. c, _ := gin.CreateTestContext(w)
  81. c.Request = httptest.NewRequest(http.MethodPost, EndpointMessages, nil)
  82. before := c.Writer.Size()
  83. c.JSON(http.StatusBadGateway, gin.H{
  84. "type": "error",
  85. "error": gin.H{
  86. "type": "upstream_error",
  87. "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",
  88. },
  89. })
  90. reported := gatewayForwardErrorAlreadyCommunicated(c, before, errors.New("upstream error: 400 message=version too low"))
  91. require.True(t, reported)
  92. body := w.Body.String()
  93. assert.NotContains(t, body, `data: {"type":"error"`)
  94. })
  95. t.Run("sse ping still needs fallback", func(t *testing.T) {
  96. w := httptest.NewRecorder()
  97. c, _ := gin.CreateTestContext(w)
  98. c.Request = httptest.NewRequest(http.MethodPost, EndpointMessages, nil)
  99. c.Header("Content-Type", "text/event-stream")
  100. before := c.Writer.Size()
  101. _, _ = c.Writer.WriteString(":\n\n")
  102. reported := gatewayForwardErrorAlreadyCommunicated(c, before, errors.New("stream read error: unexpected EOF"))
  103. require.False(t, reported)
  104. })
  105. t.Run("no write still needs fallback", func(t *testing.T) {
  106. w := httptest.NewRecorder()
  107. c, _ := gin.CreateTestContext(w)
  108. c.Request = httptest.NewRequest(http.MethodPost, EndpointMessages, nil)
  109. reported := gatewayForwardErrorAlreadyCommunicated(c, c.Writer.Size(), errors.New("upstream request failed"))
  110. require.False(t, reported)
  111. })
  112. // apikey 场景核心回归:复刻 GatewayService.handleErrorResponse 的 case 400 ——
  113. // 原样透传上游 JSON body 后返回 err。此时错误已经完整告知客户端,
  114. // handler 不得再追加 data:{"type":"error"} 帧,否则响应被污染成「JSON + 一行 data:」。
  115. t.Run("upstream 400 json passthrough via c.Data", func(t *testing.T) {
  116. w := httptest.NewRecorder()
  117. c, _ := gin.CreateTestContext(w)
  118. c.Request = httptest.NewRequest(http.MethodPost, EndpointMessages, nil)
  119. before := c.Writer.Size()
  120. 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"}}`)
  121. c.Data(http.StatusBadRequest, "application/json", upstreamBody)
  122. reported := gatewayForwardErrorAlreadyCommunicated(c, before, errors.New("upstream error: 400 message=version too low"))
  123. require.True(t, reported)
  124. body := w.Body.String()
  125. assert.NotContains(t, body, `data: {"type":"error"`)
  126. // 客户端只应收到上游那一份错误,没有被追加第二份。
  127. assert.Equal(t, 1, strings.Count(body, `"type":"error"`))
  128. })
  129. // 流式已开始(已 flush 真实 SSE 事件,不只是 ping)+ 上游中途 400:
  130. // HTTP 200 已固化,仍需 handler 补协议级终止帧,故不算「已完整告知」。
  131. t.Run("streaming 400 mid-stream still needs fallback", func(t *testing.T) {
  132. w := httptest.NewRecorder()
  133. c, _ := gin.CreateTestContext(w)
  134. c.Request = httptest.NewRequest(http.MethodPost, EndpointMessages, nil)
  135. c.Header("Content-Type", "text/event-stream")
  136. before := c.Writer.Size()
  137. _, _ = c.Writer.WriteString("event: message_start\ndata: {\"type\":\"message_start\"}\n\n")
  138. reported := gatewayForwardErrorAlreadyCommunicated(c, before, errors.New("upstream error: 400 message=version too low"))
  139. require.False(t, reported)
  140. })
  141. // 防御边界:err 为 nil 时永远不算「已告知」,避免在成功路径误吞兜底逻辑。
  142. t.Run("nil error never reports communicated", func(t *testing.T) {
  143. w := httptest.NewRecorder()
  144. c, _ := gin.CreateTestContext(w)
  145. c.Request = httptest.NewRequest(http.MethodPost, EndpointMessages, nil)
  146. c.JSON(http.StatusOK, gin.H{"ok": true})
  147. reported := gatewayForwardErrorAlreadyCommunicated(c, 0, nil)
  148. require.False(t, reported)
  149. })
  150. }