fix(openai): fail over on chat transport errors

This commit is contained in:
hsn
2026-06-23 22:53:20 +08:00
parent 85a3b12254
commit 65fa728921
3 changed files with 53 additions and 24 deletions
@@ -241,18 +241,7 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
}
resp, err := s.httpUpstream.Do(upstreamReq, proxyURL, account.ID, account.Concurrency)
if err != nil {
safeErr := sanitizeUpstreamErrorMessage(err.Error())
setOpsUpstreamError(c, 0, safeErr, "")
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
Platform: account.Platform,
AccountID: account.ID,
AccountName: account.Name,
UpstreamStatusCode: 0,
Kind: "request_error",
Message: safeErr,
})
writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed")
return nil, fmt.Errorf("upstream request failed: %s", safeErr)
return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, false)
}
defer func() { _ = resp.Body.Close() }()
@@ -171,18 +171,7 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
}
resp, err := s.httpUpstream.Do(upstreamReq, proxyURL, account.ID, account.Concurrency)
if err != nil {
safeErr := sanitizeUpstreamErrorMessage(err.Error())
setOpsUpstreamError(c, 0, safeErr, "")
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
Platform: account.Platform,
AccountID: account.ID,
AccountName: account.Name,
UpstreamStatusCode: 0,
Kind: "request_error",
Message: safeErr,
})
writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed")
return nil, fmt.Errorf("upstream request failed: %s", safeErr)
return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, false)
}
defer func() { _ = resp.Body.Close() }()
@@ -11,6 +11,8 @@ import (
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
@@ -37,6 +39,21 @@ func newOpenAITransportErrTestContext() (*gin.Context, *httptest.ResponseRecorde
return c, rec
}
type failingOpenAIHTTPUpstream struct {
err error
calls int
}
func (u *failingOpenAIHTTPUpstream) Do(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
u.calls++
return nil, u.err
}
func (u *failingOpenAIHTTPUpstream) DoWithTLS(_ *http.Request, _ string, _ int64, _ int, _ *tlsfingerprint.Profile) (*http.Response, error) {
u.calls++
return nil, u.err
}
// A durable proxy/credential failure must (a) temporarily unschedule the account
// so it stops being hammered, and (b) return a failover error so the handler
// switches to a healthy account instead of writing a hard 502 itself.
@@ -159,3 +176,37 @@ func TestHandleOpenAIUpstreamTransportError_DeadlineExceeded_StillFailsOver(t *t
var fo *UpstreamFailoverError
require.True(t, errors.As(err, &fo), "context.DeadlineExceeded must still return *UpstreamFailoverError")
}
func TestForwardAsRawChatCompletions_TransportErrorFailsOver(t *testing.T) {
repo := &openaiTransportAccountRepoStub{}
upstream := &failingOpenAIHTTPUpstream{
err: errors.New(`Post "https://opencode.ai/zen/v1/chat/completions": EOF`),
}
svc := &OpenAIGatewayService{
accountRepo: repo,
httpUpstream: upstream,
cfg: &config.Config{
Security: config.SecurityConfig{
URLAllowlist: config.URLAllowlistConfig{Enabled: false},
},
},
}
account := &Account{
ID: 81,
Name: "oc-20053",
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: map[string]any{"api_key": "sk-test", "base_url": "https://opencode.ai/zen/v1"},
}
c, rec := newOpenAITransportErrTestContext()
body := []byte(`{"model":"deepseek-v4-flash-free","messages":[{"role":"user","content":"hello"}]}`)
_, err := svc.forwardAsRawChatCompletions(context.Background(), c, account, body, "")
require.Equal(t, 1, upstream.calls)
var fo *UpstreamFailoverError
require.True(t, errors.As(err, &fo), "transport error must trigger account failover")
require.Equal(t, http.StatusBadGateway, fo.StatusCode)
require.Empty(t, repo.tempUnschedCalls, "plain EOF is transient: fail over but do not evict")
require.Equal(t, 0, rec.Body.Len(), "service must not write a hard 502 before handler can fail over")
}