fix(claude): map request timeout status to timeout_error

- Map `http.StatusRequestTimeout` (408) to Claude `timeout_error` response type

Closes: #6298
This commit is contained in:
Luis Pater
2026-10-03 15:03:30 +08:00
parent 6b037f614c
commit 42d484f9a2
2 changed files with 80 additions and 1 deletions

View File

@@ -459,7 +459,7 @@ func claudeErrorTypeFromStatus(status int) string {
return "request_too_large"
case http.StatusTooManyRequests:
return "rate_limit_error"
case http.StatusGatewayTimeout:
case http.StatusRequestTimeout, http.StatusGatewayTimeout:
return "timeout_error"
case 529:
return "overloaded_error"

View File

@@ -4,6 +4,7 @@ import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -14,6 +15,84 @@ import (
"github.com/tidwall/gjson"
)
func TestClaudeErrorTypeFromStatus(t *testing.T) {
for _, tt := range []struct {
status int
want string
}{
{http.StatusBadRequest, "invalid_request_error"},
{http.StatusRequestTimeout, "timeout_error"},
{http.StatusTooManyRequests, "rate_limit_error"},
{http.StatusInternalServerError, "api_error"},
{http.StatusGatewayTimeout, "timeout_error"},
} {
t.Run(http.StatusText(tt.status), func(t *testing.T) {
if got := claudeErrorTypeFromStatus(tt.status); got != tt.want {
t.Fatalf("error.type = %q, want %q", got, tt.want)
}
})
}
}
func TestClaudeIncompleteStreamError(t *testing.T) {
const message = "stream error: stream disconnected before completion: stream closed before response.completed"
for _, streaming := range []bool{false, true} {
name := "JSON"
if streaming {
name = "committed SSE"
}
t.Run(name, func(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
handler := NewClaudeCodeAPIHandler(&handlers.BaseAPIHandler{})
msg := &interfaces.ErrorMessage{
StatusCode: http.StatusRequestTimeout,
Error: errors.New(message),
}
wantStatus := http.StatusRequestTimeout
if streaming {
c.Header("Content-Type", "text/event-stream")
_, _ = c.Writer.Write([]byte("event: message_start\ndata: {}\n\n"))
c.Writer.Flush()
errs := make(chan *interfaces.ErrorMessage, 1)
errs <- msg
close(errs)
handler.forwardClaudeStream(c, c.Writer, func(err error) {
if err != msg.Error {
t.Errorf("cancel error = %v, want %v", err, msg.Error)
}
}, nil, errs)
wantStatus = http.StatusOK
} else {
handler.WriteErrorResponse(c, msg)
}
if recorder.Code != wantStatus {
t.Fatalf("status = %d, want %d", recorder.Code, wantStatus)
}
body := recorder.Body.String()
if streaming {
const prefix = "event: error\ndata: "
if strings.Count(body, prefix) != 1 {
t.Fatalf("expected one terminal error event; body=%s", body)
}
_, body, _ = strings.Cut(body, prefix)
body = strings.TrimSpace(body)
}
if got := gjson.Get(body, "type").String(); got != "error" {
t.Errorf("type = %q, want error", got)
}
if got := gjson.Get(body, "error.type").String(); got != "timeout_error" {
t.Errorf("error.type = %q, want timeout_error", got)
}
if got := gjson.Get(body, "error.message").String(); got != message {
t.Errorf("error.message = %q, want %q", got, message)
}
})
}
}
func TestClaudeErrorExtractsOpenAIStyleUpstreamJSON(t *testing.T) {
handler := &ClaudeCodeAPIHandler{}
msg := &interfaces.ErrorMessage{