{"record":{"id":"660dd10166d6b0c4","repo":"router-for-me/CLIProxyAPI","slug":"parse-redirect-base-w","errorCode":null,"errorMessage":"parse redirect base: %w","messagePattern":"parse redirect base: %w","errorType":"http","errorClass":null,"httpStatus":502,"severity":"warning","filePath":"internal/pluginstore/github.go","lineNumber":250,"sourceCode":"}\n\nfunc pluginStoreRedirectStatus(status int) bool {\n\tswitch status {\n\tcase http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:\n\t\treturn true\n\tdefault:\n\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 {","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/github.go#L232-L268","documentation":"In pluginStoreRedirectURL the current (request) URL is parsed with url.Parse to serve as the base for resolving a relative Location. 'parse redirect base: %w' wraps that parse failure — the URL already being fetched could not be parsed as an absolute URL. It is a defensive check; normally the earlier request construction would have failed first.","triggerScenarios":"url.Parse(requestURL) failing on the in-flight URL — control characters or a truly malformed URL that somehow passed request creation (e.g. a non-standard HTTPDoer accepting the request). Note url.Parse is lenient; failures here require control characters or similar.","commonSituations":"Custom HTTPDoer implementations or tests injecting odd URLs; URLs containing raw CR/LF introduced dynamically between request creation and redirect handling.","solutions":["Validate/normalize the URL (url.Parse + check Scheme/Host) before handing it to the plugin store client.","url.PathEscape any dynamic path components when building URLs."],"exampleFix":"// before\nu := fmt.Sprintf(\"https://host/plugins/%s\", name) // name='my plugin' (space)\n\n// after\nu := fmt.Sprintf(\"https://host/plugins/%s\", url.PathEscape(name))","handlingStrategy":"validation","validationCode":"if _, err := url.Parse(rawURL); err != nil {\n    return fmt.Errorf(\"unparseable plugin url %q: %w\", rawURL, err)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate URLs once at the boundary (config load) instead of relying on inner defensive checks.","Escape dynamic URL components."],"tags":["http","url-validation","redirects","defensive-check"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}