router-for-me/CLIProxyAPI · error
stopped after %d redirects
Error message
stopped after %d redirects
What it means
The plugin store's HTTP getter follows 3xx redirects manually (statuses 301/302/303/307/308) with a counter capped at maxPluginStoreRedirects = 10. 'stopped after %d redirects' is returned when a redirect is received and the counter is already at the limit — the client refuses to follow an eleventh hop, protecting against redirect loops.
Source
Thrown at internal/pluginstore/github.go:183
headers.Del(name)
}
if resp != nil && resp.Request != nil {
resp.Request.Header = nil
}
}
if errDo != nil {
return nil, errDo
}
if pluginStoreRedirectStatus(resp.StatusCode) {
nextURL, errRedirect := pluginStoreRedirectURL(resp, currentURL)
if errClose := resp.Body.Close(); errClose != nil {
log.WithError(errClose).Debug("failed to close plugin store redirect body")
}
if errRedirect != nil {
return nil, errRedirect
}
if redirects >= maxPluginStoreRedirects {
return nil, fmt.Errorf("stopped after %d redirects", maxPluginStoreRedirects)
}
currentURL = nextURL
continue
}
return readPluginStoreResponse(resp, maxSize, authenticated)
}
}
func (c Client) httpClient() HTTPDoer {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
func (c Client) userAgent() string {
if strings.TrimSpace(c.UserAgent) != "" {
return strings.TrimSpace(c.UserAgent)View on GitHub (pinned to 78f0c4079e)
Solutions
- Resolve the final artifact URL once with curl -IL and update the manifest/URL to the redirect target to shorten the chain.
- If a loop is involved (URL A -> B -> A), fix the server-side redirect or the misconfigured mirror.
- For private repos, ensure GitHub auth is configured so the API download URL is used directly instead of bouncing through sign-in redirects (see releaseAssetAPIAuthenticated).
- Re-publish assets under stable, direct URLs.
Example fix
# before # manifest url bounces through 11 redirects (mirror loop) curl -IL https://mirror.example.com/p-linux-amd64 # ends in a loop # after # resolve the final URL and pin it in the manifest curl -IL https://mirror.example.com/p-linux-amd64 | grep -i '^location' # manifest: url: https://final-host.example.com/p-linux-amd64
Defensive patterns
Strategy: retry
Try / catch
data, err := client.get(ctx, artifact.URL, "application/octet-stream", kind, 0)
if err != nil {
if strings.Contains(err.Error(), "stopped after") && strings.Contains(err.Error(), "redirects") {
// resolve and pin the final URL, then retry once
final := resolveFinalURL(ctx, artifact.URL)
if final != "" && final != artifact.URL {
artifact.URL = final
data, err = client.get(ctx, artifact.URL, "application/octet-stream", kind, 0)
}
}
if err != nil { return err }
} Prevention
- Pin final (post-redirect) URLs in manifests.
- Keep redirect chains short; avoid chains of mirrors.
- Configure auth for private repos so sign-in redirects do not loop.
When it happens
Trigger: Any plugin store GET (metadata or artifact) whose redirect chain exceeds 10 hops: an owner/repo redirect loop on GitHub, a CDN misconfiguration bouncing between two URLs, or auth flows that keep redirecting because credentials are not attached.
Common situations: A renamed GitHub repository whose release asset URLs redirect through multiple hops; two mirrors pointing at each other; S3/CloudFront presigned-URL chains that expire and redirect to a login page that redirects back.
Related errors
- redirect missing Location header
- parse redirect location: %w
- redirect location is not absolute
- Codex live request body too large
- Codex live session field must contain valid JSON
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/3b09cd679b3648ba.
Report an issue: GitHub.