{"record":{"id":"3425549d37dfcdfb","repo":"router-for-me/CLIProxyAPI","slug":"redirect-location-is-not-absolute","errorCode":null,"errorMessage":"redirect location is not absolute","messagePattern":"redirect location is not absolute","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/pluginstore/github.go","lineNumber":257,"sourceCode":"\t\treturn false\n\t}\n}\n\nfunc pluginStoreRedirectURL(resp *http.Response, requestURL string) (string, error) {\n\tlocation := strings.TrimSpace(resp.Header.Get(\"Location\"))\n\tif location == \"\" {\n\t\treturn \"\", fmt.Errorf(\"redirect missing Location header\")\n\t}\n\tbase, errBase := url.Parse(requestURL)\n\tif errBase != nil {\n\t\treturn \"\", fmt.Errorf(\"parse redirect base: %w\", errBase)\n\t}\n\tnext, errNext := base.Parse(location)\n\tif errNext != nil {\n\t\treturn \"\", fmt.Errorf(\"parse redirect location: %w\", errNext)\n\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)","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/github.go#L239-L275","documentation":"Once Location is resolved against the base URL, pluginStoreRedirectURL requires the result to be absolute (non-empty Scheme and Host). 'redirect location is not absolute' fires when the resolved next URL still lacks scheme/host — possible when the base itself was relative (so resolution could not produce an absolute result) or the Location is scheme-relative like '//host/path' resolved against a scheme-less base.","triggerScenarios":"A Location such as '//cdn.example.com/file' or '/file' resolved against a request URL that itself has no scheme/host (e.g. the client was invoked with a relative or path-only URL, or a custom HTTPDoer context). Scheme-relative Locations against a proper https base resolve fine, so the usual culprit is a relative base URL.","commonSituations":"Calling the plugin store client with a relative base URL in tests or behind a URL-rewriting proxy; misconfigured manifests with path-only URLs.","solutions":["Ensure the URL passed into the plugin store client is absolute (scheme + host), e.g. https://api.github.com/... or a full https artifact URL.","Fix manifest URLs that are stored as paths without a host.","If a server emits scheme-relative Locations, that is fine as long as your base URL is absolute — verify the base first."],"exampleFix":"# before (manifest)\nurl: /downloads/p-linux-amd64   # relative -> 'redirect location is not absolute' on redirect\n\n# after (manifest)\nurl: https://example.com/downloads/p-linux-amd64","handlingStrategy":"validation","validationCode":"u, err := url.Parse(baseURL)\nif err != nil || u.Scheme == \"\" || u.Host == \"\" {\n    return fmt.Errorf(\"plugin url must be absolute, got %q\", baseURL)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Store full absolute URLs (scheme+host) in plugin manifests and config.","Reject path-only URLs at config validation time."],"tags":["http","redirects","url-validation","configuration","plugin-store"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}