{"record":{"id":"71e20336cdc1b853","repo":"MHSanaei/3x-ui","slug":"stopped-after-10-redirects","errorCode":null,"errorMessage":"stopped after 10 redirects","messagePattern":"stopped after 10 redirects","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/web/service/outbound_subscription.go","lineNumber":350,"sourceCode":"\t// any direct DB tampering). Private targets are blocked unless this\n\t// subscription was explicitly created with AllowPrivate.\n\tcleanURL, err := SanitizePublicHTTPURL(sub.Url, sub.AllowPrivate)\n\tif err != nil {\n\t\ts.recordError(sub, err)\n\t\treturn nil, err\n\t}\n\tif cleanURL == \"\" {\n\t\treturn nil, common.NewError(\"subscription has no valid URL\")\n\t}\n\tsub.Url = cleanURL // persist the cleaned version\n\n\tclient := s.subscriptionFetchClient(30*time.Second, sub.AllowInsecure)\n\t// Re-validate every redirect hop: the initial host is checked above, but a\n\t// redirect could still point at a private/internal address (SSRF). Cap the\n\t// redirect chain as well.\n\tclient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\tif len(via) >= 10 {\n\t\t\treturn fmt.Errorf(\"stopped after 10 redirects\")\n\t\t}\n\t\tif sub.AllowPrivate {\n\t\t\treturn nil\n\t\t}\n\t\tctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)\n\t\tdefer cancel()\n\t\treturn rejectPrivateHost(ctx, req.URL.Hostname())\n\t}\n\n\treqCtx := netsafe.ContextWithAllowPrivate(context.Background(), sub.AllowPrivate)\n\treq, err := http.NewRequestWithContext(reqCtx, http.MethodGet, sub.Url, nil)\n\tif err != nil {\n\t\ts.recordError(sub, err)\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"User-Agent\", \"3x-ui-outbound-sub/1.0\")\n\n\tresp, err := client.Do(req)","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/service/outbound_subscription.go#L332-L368","documentation":"The subscription fetch client installs a CheckRedirect hook that re-validates every hop against private addresses (SSRF guard) and hard-caps the chain at 10 via the standard net/http idiom: returning an error when len(via) >= 10. 'stopped after 10 redirects' fires exactly when the 11th consecutive redirect is attempted — independent of the AllowPrivate setting.","triggerScenarios":"The subscription URL (after the initial host validation) redirects more than 10 times in a chain — redirect loops (A -> B -> A), or a CDN/login wall that keeps bouncing.","commonSituations":"Expired provider session redirecting to a login page that redirects back; misconfigured shortener loop; a provider that moved and chained forwarding rules.","solutions":["curl -sIL <url> and count the Location hops to find the loop","Update the subscription URL to the final canonical endpoint","Log in again / refresh the token if the provider requires auth and is bouncing to a login page"],"exampleFix":"# before\nsub.Url = \"https://s.example/r/abc\"   # loop of 12 redirects\n\n# after\nsub.Url = \"https://provider.example/api/sub?key=...\"  # final destination from the last Location header","handlingStrategy":"validation","validationCode":"// pre-resolve the redirect chain once\nif err := checkRedirectDepth(sub.Url, 10); err != nil {\n    return fmt.Errorf(\"subscription URL redirect problem: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"_, err := client.Do(req)\nif err != nil && strings.Contains(err.Error(), \"stopped after 10 redirects\") {\n    // url.Error wraps it; fix/replace the subscription URL, no point retrying\n}","preventionTips":["Store the final canonical subscription URL, not shortlinks","Periodically validate stored subscription URLs with a HEAD chain walk"],"tags":["network","redirects","subscriptions","ssrf"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}