Merge pull request #4540 from heathermhuang/codex/investigate-grok-oauth-test-4525
fix(grok): retry CLI chat permission denial
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -323,7 +324,7 @@ func (t *grokAccessDeniedFallbackTransport) RoundTrip(req *http.Request) (*http.
|
||||
}
|
||||
|
||||
body, ok := bufferSmallResponseBody(resp, grokFallbackBodyLimit)
|
||||
if !ok || !bytes.Contains(bytes.ToLower(body), []byte("access denied")) {
|
||||
if !ok || !isGrokCLICompatibilityAccessDenied(body) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -350,6 +351,22 @@ func (t *grokAccessDeniedFallbackTransport) RoundTrip(req *http.Request) (*http.
|
||||
return fallbackResp, nil
|
||||
}
|
||||
|
||||
func isGrokCLICompatibilityAccessDenied(body []byte) bool {
|
||||
lower := bytes.ToLower(body)
|
||||
if bytes.Contains(lower, []byte("access denied")) {
|
||||
return true
|
||||
}
|
||||
var payload struct {
|
||||
Code string `json:"code"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil || !strings.EqualFold(strings.TrimSpace(payload.Code), "permission_denied") {
|
||||
return false
|
||||
}
|
||||
const chatEndpointDeniedPrefix = "access to the chat endpoint is denied. please ensure you're using the correct credentials. if you believe this is a mistake, please"
|
||||
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(payload.Error)), chatEndpointDeniedPrefix)
|
||||
}
|
||||
|
||||
func isGrokCLIAccessDeniedFallbackCandidate(req *http.Request, resp *http.Response) bool {
|
||||
return req != nil && req.URL != nil && req.GetBody != nil && resp != nil &&
|
||||
resp.StatusCode == http.StatusForbidden &&
|
||||
|
||||
@@ -311,6 +311,119 @@ func TestHTTPUpstreamDoFallsBackToOfficialGrokAPIOnCLIAccessDenied(t *testing.T)
|
||||
require.Empty(t, fallbackHeaders.Get("User-Agent"))
|
||||
}
|
||||
|
||||
func TestGrokAccessDeniedFallbackRecognizesChatEndpointPermissionDenied(t *testing.T) {
|
||||
var hosts []string
|
||||
transport := &grokAccessDeniedFallbackTransport{
|
||||
base: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
hosts = append(hosts, req.URL.Hostname())
|
||||
if req.URL.Hostname() == grokCLIProxyHost {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(
|
||||
`{"code":"permission_denied","error":"Access to the chat endpoint is denied. Please ensure you're using the correct credentials. If you believe this is a mistake, please contact support."}`,
|
||||
)),
|
||||
Request: req,
|
||||
}, nil
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(`{"id":"response-ok"}`)),
|
||||
Request: req,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", strings.NewReader(`{"model":"grok-4.5"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer oauth-token")
|
||||
req.Header.Set("X-XAI-Token-Auth", "xai-grok-cli")
|
||||
|
||||
resp, err := transport.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, []string{grokCLIProxyHost, grokOfficialAPIHost}, hosts)
|
||||
}
|
||||
|
||||
func TestIsGrokCLICompatibilityAccessDenied(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want bool
|
||||
}{
|
||||
{name: "legacy compatibility wording", body: `{"error":"Access denied"}`, want: true},
|
||||
{
|
||||
name: "observed chat endpoint permission denial",
|
||||
body: `{"code":"permission_denied","error":"Access to the chat endpoint is denied. Please ensure you're using the correct credentials. If you believe this is a mistake, please contact support."}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "entitlement denial using the same broad terms",
|
||||
body: `{"code":"permission_denied","error":"Access to the chat endpoint is denied because a subscription is required"}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different permission denied endpoint",
|
||||
body: `{"code":"permission_denied","error":"Access to the billing endpoint is denied."}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wrong structured error code",
|
||||
body: `{"code":"subscription_required","error":"Access to the chat endpoint is denied. Please ensure you're using the correct credentials. If you believe this is a mistake, please contact support."}`,
|
||||
want: false,
|
||||
},
|
||||
{name: "malformed response", body: `permission_denied: chat endpoint denied`, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, isGrokCLICompatibilityAccessDenied([]byte(tt.body)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGrokCLIAccessDeniedFallbackCandidateRequiresAuthenticatedReplayableCLI403(t *testing.T) {
|
||||
newRequest := func() *http.Request {
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", strings.NewReader(`{"model":"grok-4.5"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer oauth-token")
|
||||
req.Header.Set("X-XAI-Token-Auth", "xai-grok-cli")
|
||||
return req
|
||||
}
|
||||
newResponse := func() *http.Response { return &http.Response{StatusCode: http.StatusForbidden} }
|
||||
|
||||
t.Run("valid candidate", func(t *testing.T) {
|
||||
require.True(t, isGrokCLIAccessDeniedFallbackCandidate(newRequest(), newResponse()))
|
||||
})
|
||||
t.Run("non CLI host", func(t *testing.T) {
|
||||
req := newRequest()
|
||||
req.URL.Host = "api.x.ai"
|
||||
require.False(t, isGrokCLIAccessDeniedFallbackCandidate(req, newResponse()))
|
||||
})
|
||||
t.Run("missing CLI identity", func(t *testing.T) {
|
||||
req := newRequest()
|
||||
req.Header.Del("X-XAI-Token-Auth")
|
||||
require.False(t, isGrokCLIAccessDeniedFallbackCandidate(req, newResponse()))
|
||||
})
|
||||
t.Run("missing bearer authentication", func(t *testing.T) {
|
||||
req := newRequest()
|
||||
req.Header.Del("Authorization")
|
||||
require.False(t, isGrokCLIAccessDeniedFallbackCandidate(req, newResponse()))
|
||||
})
|
||||
t.Run("non forbidden response", func(t *testing.T) {
|
||||
resp := newResponse()
|
||||
resp.StatusCode = http.StatusUnauthorized
|
||||
require.False(t, isGrokCLIAccessDeniedFallbackCandidate(newRequest(), resp))
|
||||
})
|
||||
t.Run("non replayable request", func(t *testing.T) {
|
||||
req := newRequest()
|
||||
req.GetBody = nil
|
||||
require.False(t, isGrokCLIAccessDeniedFallbackCandidate(req, newResponse()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPUpstreamDoDoesNotFallbackForGrokEntitlementDenial(t *testing.T) {
|
||||
transport := &grokAccessDeniedFallbackTransport{
|
||||
base: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
|
||||
Reference in New Issue
Block a user