{"record":{"id":"9306121759e7796c","repo":"router-for-me/CLIProxyAPI","slug":"unexpected-status-d-s-930612","errorCode":null,"errorMessage":"unexpected status %d: %s","messagePattern":"unexpected status (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/pluginstore/github.go","lineNumber":273,"sourceCode":"\t}\n\tif next.Scheme == \"\" || next.Host == \"\" {\n\t\treturn \"\", fmt.Errorf(\"redirect location is not absolute\")\n\t}\n\treturn next.String(), nil\n}\n\nfunc readPluginStoreResponse(resp *http.Response, maxSize int64, authenticated bool) ([]byte, error) {\n\tdefer func() {\n\t\tif errClose := resp.Body.Close(); errClose != nil {\n\t\t\tlog.WithError(errClose).Debug(\"failed to close plugin store response body\")\n\t\t}\n\t}()\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {\n\t\tif authenticated {\n\t\t\treturn nil, fmt.Errorf(\"unexpected status %d\", resp.StatusCode)\n\t\t}\n\t\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))\n\t\treturn nil, fmt.Errorf(\"unexpected status %d: %s\", resp.StatusCode, strings.TrimSpace(string(body)))\n\t}\n\treader := io.Reader(resp.Body)\n\tif maxSize > 0 {\n\t\treader = io.LimitReader(resp.Body, maxSize+1)\n\t}\n\tdata, errRead := io.ReadAll(reader)\n\tif errRead != nil {\n\t\treturn nil, fmt.Errorf(\"read response: %w\", errRead)\n\t}\n\tif maxSize > 0 && int64(len(data)) > maxSize {\n\t\treturn nil, fmt.Errorf(\"response exceeds maximum allowed size of %d bytes\", maxSize)\n\t}\n\treturn data, nil\n}\n\nfunc pluginStoreRequestError(requestURL string, err error) error {\n\tparsed, errParse := url.Parse(strings.TrimSpace(requestURL))\n\tsafeURL := \"plugin store url\"","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/github.go#L255-L291","documentation":"The unauthenticated counterpart of 617: readPluginStoreResponse appends up to 4KB of the response body to the status error ('unexpected status %d: %s') when no credentials were attached, since an unauthenticated error body carries nothing sensitive. This is the error you see for public endpoints like api.github.com without a token — most notably GitHub rate limiting.","triggerScenarios":"Unauthenticated GETs to api.github.com or artifact hosts returning non-2xx: 403 with an API rate-limit exceeded message, 404 for unknown repos, or artifact hosts returning error pages. The body snippet shows the server's own explanation (e.g. 'API rate limit exceeded for 1.2.3.4').","commonSituations":"Anonymous GitHub API rate limits (60 req/hour per IP) hit in CI or shared NATs; typos in owner/repo; deleted releases; artifact hosts returning 403 for hotlinking or expired signed URLs.","solutions":["Read the body snippet in the error — for rate limit, configure a GitHub token in plugin store auth so requests are authenticated (5000/hr) and take the 617 path instead.","404: verify the repository owner/name and release tag in the plugin manifest.","403 from artifact hosts: re-pin the manifest URL to a fresh, valid link.","Add retry-with-backoff for transient 5xx bodies."],"exampleFix":"# before\n# 'unexpected status 403: API rate limit exceeded for 203.0.113.9.'\n\n# after\n# configure credentials so metadata requests are authenticated:\n# auth:\n#   - match: https://api.github.com/\n#     kind: metadata\n#     token: <github-pat>","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"data, err := client.get(ctx, requestURL, accept, kind, 0)\nif err != nil {\n    msg := err.Error()\n    if strings.Contains(msg, \"unexpected status 403\") && strings.Contains(msg, \"rate limit\") {\n        time.Sleep(time.Hour) // or configure a token to move to the authenticated path\n    } else if strings.Contains(msg, \"unexpected status 5\") {\n        time.Sleep(backoff) // exponential backoff, few attempts\n    }\n}","preventionTips":["Configure a GitHub token for plugin store metadata requests to escape the 60/hr anonymous limit.","Cache release metadata between installs to cut API calls.","Read the body snippet the error carries — it states the server's reason."],"tags":["http-status","rate-limit","github-api","network","plugin-store"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}