Merge pull request #4540 from heathermhuang/codex/investigate-grok-oauth-test-4525

fix(grok): retry CLI chat permission denial
This commit is contained in:
Wesley Liddick
2026-07-18 20:50:07 +08:00
committed by GitHub
2 changed files with 131 additions and 1 deletions
+18 -1
View File
@@ -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) {