mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
a35f71cd8a
Mid-stream HTTP/2 peer resets from LLM providers can arrive after a 200 streaming response has already emitted provisional parts. Previously those resets fell through as generic non-retryable errors because `stream ID` messages did not match retryable transport signals, and stream IDs could be misread as HTTP statuses. Classify retryable HTTP/2 RST_STREAM codes as transient timeout failures, ignore stream IDs during status extraction, and keep the existing `retry` event as the rollback boundary for provisional message parts so replacement attempts do not replay failed-attempt output. Closes CODAGT-382
73 lines
2.9 KiB
Go
73 lines
2.9 KiB
Go
package chaterror_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chaterror"
|
|
)
|
|
|
|
func TestExtractStatusCode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want int
|
|
}{
|
|
{name: "Status", input: "received status 429 from upstream", want: 429},
|
|
{name: "StatusCode", input: "status code: 503", want: 503},
|
|
{name: "HTTP", input: "http 502 bad gateway", want: 502},
|
|
{name: "Standalone", input: "got 504 from upstream", want: 504},
|
|
{name: "MultipleStandaloneCodesReturnFirstMatch", input: "retrying 503 after 429", want: 503},
|
|
{name: "MixedCaseViaCallerLowering", input: "HTTP 503 bad gateway", want: 503},
|
|
{name: "PortNumberIPIsNotStatus", input: "dial tcp 10.0.0.1:503: connection refused", want: 0},
|
|
{name: "PortNumberHostIsNotStatus", input: "proxy.internal:502 unreachable", want: 0},
|
|
{name: "PortNumberDialIsNotStatus", input: "dial tcp 172.16.0.5:429: refused", want: 0},
|
|
{name: "PortThenRealStatusReturnsRealStatus", input: "proxy at 10.0.0.1:500 returned 503", want: 503},
|
|
{name: "HTTP2StreamIDIsNotStatus", input: "stream error: stream ID 401; INTERNAL_ERROR; received from peer", want: 0},
|
|
{name: "HTTP2StreamIDWithPunctuationIsNotStatus", input: "stream error: stream ID: 503; PROTOCOL_ERROR; received from peer", want: 0},
|
|
{name: "HTTP2StreamIDThenExplicitStatusReturnsStatus", input: "stream error: stream ID 455; status 503 from upstream", want: 503},
|
|
{name: "NoFabricatedOverloadStatus", input: "anthropic overloaded_error", want: 0},
|
|
{name: "NoFabricatedRateLimitStatus", input: "too many requests", want: 0},
|
|
{name: "NoFabricatedBadGatewayStatus", input: "bad gateway", want: 0},
|
|
{name: "NoFabricatedServiceUnavailableStatus", input: "service unavailable", want: 0},
|
|
{name: "NoStatus", input: "boom", want: 0},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tt.want, chaterror.ExtractStatusCodeForTest(strings.ToLower(tt.input)))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{name: "OpenAICompatBeatsOpenAI", input: "openai-compat upstream error", want: "openai-compat"},
|
|
{name: "OpenAICompatibleAlias", input: "openai compatible proxy", want: "openai-compat"},
|
|
{name: "AzureOpenAI", input: "azure openai rate limited", want: "azure"},
|
|
{name: "OpenAI", input: "openai rate limited", want: "openai"},
|
|
{name: "Anthropic", input: "anthropic overloaded", want: "anthropic"},
|
|
{name: "GoogleGemini", input: "gemini timeout", want: "google"},
|
|
{name: "Vercel", input: "vercel ai gateway 503", want: "vercel"},
|
|
{name: "Unknown", input: "local provider error", want: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tt.want, chaterror.DetectProviderForTest(strings.ToLower(tt.input)))
|
|
})
|
|
}
|
|
}
|