{"record":{"id":"b31bfbd5ac04ef9f","repo":"Wei-Shaw/sub2api","slug":"xai-oauth-redirected-too-many-times","errorCode":null,"errorMessage":"xAI OAuth redirected too many times","messagePattern":"xAI OAuth redirected too many times","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/internal/pkg/xai/sso_device.go","lineNumber":305,"sourceCode":"\t\tlocation := strings.TrimSpace(response.Header.Get(\"Location\"))\n\t\tif location == \"\" {\n\t\t\treturn response.StatusCode, currentURL, data, errors.New(\"xAI OAuth redirect missing Location\")\n\t\t}\n\t\tbase, _ := url.Parse(currentURL)\n\t\tnext, err := url.Parse(location)\n\t\tif err != nil {\n\t\t\treturn response.StatusCode, currentURL, data, err\n\t\t}\n\t\tcurrentURL = base.ResolveReference(next).String()\n\t\tif !safeXAIAuthURL(currentURL) {\n\t\t\treturn response.StatusCode, currentURL, data, errors.New(\"xAI OAuth redirected to untrusted host\")\n\t\t}\n\t\tif response.StatusCode == http.StatusSeeOther || ((response.StatusCode == http.StatusMovedPermanently || response.StatusCode == http.StatusFound) && currentMethod != http.MethodGet && currentMethod != http.MethodHead) {\n\t\t\tcurrentMethod = http.MethodGet\n\t\t\tcurrentForm = nil\n\t\t}\n\t}\n\treturn 0, currentURL, nil, errors.New(\"xAI OAuth redirected too many times\")\n}\n\nfunc seedSSOCookies(jar http.CookieJar, token string) {\n\tif jar == nil {\n\t\treturn\n\t}\n\tfor _, rawURL := range []string{SSOAccountsURL, OAuthIssuer + \"/\"} {\n\t\ttarget, err := url.Parse(rawURL)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tjar.SetCookies(target, []*http.Cookie{\n\t\t\t{Name: \"sso\", Value: token, Path: \"/\", Secure: true, HttpOnly: true},\n\t\t\t{Name: \"sso-rw\", Value: token, Path: \"/\", Secure: true, HttpOnly: true},\n\t\t})\n\t}\n}\n","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/backend/internal/pkg/xai/sso_device.go#L287-L323","documentation":"The manual redirect loop allows at most 8 hops (redirects 0..8 inclusive of the initial request). Exhausting the budget without a non-3xx final response yields this error. It protects against redirect loops between xAI auth endpoints.","triggerScenarios":"verify/approve endpoints bouncing between two or more URLs indefinitely (cookie not set, so each hop re-issues the redirect); or a genuinely long legitimate chain exceeding 8 hops.","commonSituations":"Cookie jar disabled or cookies rejected (Secure/SameSite attributes vs http:// test contexts), so xAI keeps redirecting to 'login' which redirects back; xAI incidents with redirect loops; region negotiation ping-pong.","solutions":["Ensure the http.Client has a cookie jar and cookies persist across hops (seedSSOCookies must have run).","Log each hop URL to identify the loop participants.","Retry with a fresh session token; stale state often causes ping-pong.","Only if a legitimate chain needs more hops, raise the loop bound in sso_device.go."],"exampleFix":"// client without cookie handling (loop-prone)\nclient := &http.Client{}\n\n// after\nclient := &http.Client{\n    Jar: jar, // required so auth cookies survive redirect hops\n    CheckRedirect: http.ErrUseLastResponse, // flow reads Location itself\n}","handlingStrategy":"validation","validationCode":"// before invoking the flow\nif client.Jar == nil {\n    return errors.New(\"SSO device flow requires an http.Client with a CookieJar\")\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"redirected too many times\") {\n    return restartFlowWithFreshToken() // loop almost always means cookie state was lost\n}","preventionTips":["Always attach a cookie jar","Seed cookies before the flow starts","Log each redirect hop when debugging"],"tags":["xai","sso","redirect","loop","cookies"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}