{"record":{"id":"dad02bcd2592b6d8","repo":"JuliusBrussee/caveman","slug":"githubapp-invalid-request-path","errorCode":null,"errorMessage":"githubapp: invalid request path","messagePattern":"githubapp: invalid request path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/githubapp/githubapp.go","lineNumber":358,"sourceCode":"// DoToken issues an authenticated GitHub REST call with an installation token and\n// returns the status + raw body for the caller to parse. It is the reusable\n// primitive the worker's PR opener builds the Git Data API flow on, so every\n// GitHub egress goes through the one SSRF-guarded client + fixed base host.\nfunc (a *App) DoToken(ctx context.Context, token, method, path string, body any) (int, []byte, error) {\n\treturn a.do(ctx, \"Bearer \"+token, method, path, body)\n}\n\nfunc (a *App) do(ctx context.Context, authorization, method, path string, body any) (int, []byte, error) {\n\tif !strings.HasPrefix(path, \"/\") || strings.HasPrefix(path, \"//\") || strings.Contains(path, \"\\\\\") {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: request path must be a single-host absolute path\")\n\t}\n\tbase, err := url.Parse(a.baseURL)\n\tif err != nil || base.Scheme == \"\" || base.Host == \"\" || base.User != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: invalid base URL\")\n\t}\n\trelative, err := url.ParseRequestURI(path)\n\tif err != nil || relative.IsAbs() || relative.Host != \"\" || relative.User != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: invalid request path\")\n\t}\n\ttarget, err := url.Parse(a.baseURL + path)\n\tif err != nil || target.Scheme != base.Scheme || !strings.EqualFold(target.Host, base.Host) || target.User != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: request path escaped configured host\")\n\t}\n\tvar reader io.Reader\n\tif body != nil {\n\t\tb, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn 0, nil, fmt.Errorf(\"githubapp: marshal request: %w\", err)\n\t\t}\n\t\treader = bytes.NewReader(b)\n\t}\n\treq, err := http.NewRequestWithContext(ctx, method, target.String(), reader)\n\tif err != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: build request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", authorization)","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/githubapp/githubapp.go#L340-L376","documentation":"After the basic single-host-path check, do() parses the request path with url.ParseRequestURI and rejects anything that is absolute (has a scheme), carries a host, or contains userinfo. The path argument must be purely relative. This error fires when the path argument is actually a full URL or otherwise unparseable.","triggerScenarios":"Passing \"https://api.github.com/repos/o/r\" as the path to DoToken; passing \"mailto:x@y\" or a path with embedded credentials like \"//user@host/x\"; passing a string with characters that break ParseRequestURI (e.g. raw spaces or control characters).","commonSituations":"Refactoring code that previously used a full-URL HTTP client and passing the whole URL as the path; concatenating a base URL into the path variable, producing a double-prefixed value; pasting a curl URL directly as the path argument.","solutions":["Pass only the relative path portion, e.g. \"/repos/octocat/hello\", and keep the host in the App's baseURL.","If you have a full URL, strip the scheme+host before calling, or construct the App with that base instead.","Encode query parameters with url.Values and interpolate via \"?\" + v.Encode() so no raw spaces/unsafe chars reach the path.","Trim stray whitespace/control characters from the path before calling."],"exampleFix":"// before\nclient.DoToken(ctx, tok, \"GET\", \"https://api.github.com/repos/octo/hello\", nil)\n// after\nclient.DoToken(ctx, tok, \"GET\", \"/repos/octo/hello\", nil)","handlingStrategy":"validation","validationCode":"u, err := url.ParseRequestURI(path)\nif err != nil || u.IsAbs() || u.Host != \"\" || u.User != nil {\n    return fmt.Errorf(\"path must be relative: %q\", path)\n}","typeGuard":null,"tryCatchPattern":"if _, _, err := app.DoToken(ctx, tok, m, path, nil); err != nil {\n    if strings.Contains(err.Error(), \"invalid request path\") {\n        // path was a full URL; strip scheme+host and retry once\n    }\n}","preventionTips":["Keep a single path-builder helper that only emits relative paths.","When migrating from full-URL clients, strip the base at the boundary.","Encode query strings with url.Values.Encode()."],"tags":["github","ssrf","url-validation","path"],"backgroundTag":"invalid-url-format","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}