Skip to content

Commit bcc76df

Browse files
authored
test: verify upstream errors are relayed to client in streaming chatcompletions (#148)
* test: verify upstream errors are relayed to client in streaming chatcompletions * chore: address comments
1 parent ca7572e commit bcc76df

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package chatcompletions
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strconv"
7+
"testing"
8+
9+
"cdr.dev/slog/v3"
10+
"cdr.dev/slog/v3/sloggers/slogtest"
11+
"github.com/coder/aibridge/config"
12+
"github.com/coder/aibridge/internal/testutil"
13+
"github.com/google/uuid"
14+
"github.com/openai/openai-go/v3"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
"go.opentelemetry.io/otel"
18+
)
19+
20+
// Test that when the upstream provider returns an error before streaming starts,
21+
// the error status code and body are correctly relayed to the client.
22+
func TestStreamingInterception_RelaysUpstreamErrorToClient(t *testing.T) {
23+
t.Parallel()
24+
25+
tests := []struct {
26+
name string
27+
statusCode int
28+
responseBody string
29+
expectedErrStr string
30+
expectedBody string
31+
}{
32+
{
33+
name: "bad request error",
34+
statusCode: http.StatusBadRequest,
35+
responseBody: `{"error":{"message":"Invalid request","type":"invalid_request_error","code":"invalid_request"}}`,
36+
expectedErrStr: strconv.Itoa(http.StatusBadRequest),
37+
expectedBody: "invalid_request",
38+
},
39+
{
40+
name: "rate limit error",
41+
statusCode: http.StatusTooManyRequests,
42+
responseBody: `{"error":{"message":"Rate limit exceeded","type":"rate_limit_error","code":"rate_limit_exceeded"}}`,
43+
expectedErrStr: strconv.Itoa(http.StatusTooManyRequests),
44+
expectedBody: "rate_limit",
45+
},
46+
{
47+
name: "internal server error",
48+
statusCode: http.StatusInternalServerError,
49+
responseBody: `{"error":{"message":"Internal server error","type":"server_error","code":"internal_error"}}`,
50+
expectedErrStr: strconv.Itoa(http.StatusInternalServerError),
51+
expectedBody: "server_error",
52+
},
53+
}
54+
55+
for _, tc := range tests {
56+
t.Run(tc.name, func(t *testing.T) {
57+
t.Parallel()
58+
59+
// Setup a mock server that returns an error immediately (before any streaming)
60+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61+
w.Header().Set("Content-Type", "application/json")
62+
w.Header().Set("x-should-retry", "false")
63+
w.WriteHeader(tc.statusCode)
64+
_, _ = w.Write([]byte(tc.responseBody))
65+
}))
66+
t.Cleanup(mockServer.Close)
67+
68+
// Create interceptor with mock server URL
69+
cfg := config.OpenAI{
70+
BaseURL: mockServer.URL,
71+
Key: "test-key",
72+
}
73+
74+
req := &ChatCompletionNewParamsWrapper{
75+
ChatCompletionNewParams: openai.ChatCompletionNewParams{
76+
Model: "gpt-4",
77+
Messages: []openai.ChatCompletionMessageParamUnion{
78+
openai.UserMessage("hello"),
79+
},
80+
},
81+
Stream: true,
82+
}
83+
84+
tracer := otel.Tracer("test")
85+
interceptor := NewStreamingInterceptor(uuid.New(), req, cfg, tracer)
86+
87+
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
88+
interceptor.Setup(logger, &testutil.MockRecorder{}, nil)
89+
90+
// Create test request
91+
w := httptest.NewRecorder()
92+
httpReq := httptest.NewRequest(http.MethodPost, "/chat/completions", nil)
93+
94+
// Process the request
95+
err := interceptor.ProcessRequest(w, httpReq)
96+
97+
// Verify error was returned
98+
require.Error(t, err)
99+
assert.Contains(t, err.Error(), tc.expectedErrStr)
100+
101+
// Verify status code was written to response
102+
assert.Equal(t, tc.statusCode, w.Code, "expected status code to be relayed to client")
103+
104+
// Verify error body contains expected error info
105+
body := w.Body.String()
106+
assert.Contains(t, body, tc.expectedBody, "expected error type in response body")
107+
})
108+
}
109+
}

0 commit comments

Comments
 (0)