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

  1. Resolve the final artifact URL once with curl -IL and update the manifest/URL to the redirect target to shorten the chain.
  2. If a loop is involved (URL A -> B -> A), fix the server-side redirect or the misconfigured mirror.
  3. 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).
  4. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/3b09cd679b3648ba. Report an issue: GitHub.