{"record":{"id":"44641767081d63d9","repo":"crowdsecurity/crowdsec","slug":"baseurl-must-have-a-trailing-slash-but-q-does-no","errorCode":null,"errorMessage":"BaseURL must have a trailing slash, but %q does not","messagePattern":"BaseURL must have a trailing slash, but %q does not","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/apiclient/client_http.go","lineNumber":23,"sourceCode":"\t\"compress/gzip\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst compressionMinSize = 5 * 1024 // 5KB\n\nfunc (c *ApiClient) PrepareRequest(ctx context.Context, method, url string, body any) (*http.Request, error) {\n\tif !strings.HasSuffix(c.BaseURL.Path, \"/\") {\n\t\treturn nil, fmt.Errorf(\"BaseURL must have a trailing slash, but %q does not\", c.BaseURL)\n\t}\n\n\tu, err := c.BaseURL.Parse(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar buf io.ReadWriter\n\tcompressedBody := false\n\n\tif body != nil {\n\t\tjsonBuf := &bytes.Buffer{}\n\t\tenc := json.NewEncoder(jsonBuf)\n\t\tenc.SetEscapeHTML(false)\n\n\t\tif err = enc.Encode(body); err != nil {\n\t\t\treturn nil, err\n\t\t}","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/apiclient/client_http.go#L5-L41","documentation":"PrepareRequest validates that the client's BaseURL path ends with a slash, because it resolves relative endpoint paths with URL.Parse, whose behavior silently drops the last path segment when the base lacks a trailing slash. The library refuses to proceed rather than send requests to the wrong path. The %q prints the whole URL, not just the path.","triggerScenarios":"An ApiClient built with a BaseURL like 'http://localhost:8080/api' (no trailing '/') calls PrepareRequest for any endpoint; typically created by passing a hand-constructed *url.URL to NewClient/RegisterClient instead of going through createTransport.","commonSituations":"Writing custom tooling against pkg/apiclient with a manually parsed api_url that trimmed the slash; config value 'http://host:8080' without a trailing slash being parsed into BaseURL without normalization; unix-socket URL construction.","solutions":["Ensure the URL used to build the client ends with '/', e.g. 'http://localhost:8080/'","Normalize in code: if !strings.HasSuffix(u.Path, \"/\") { u.Path += \"/\" } before constructing the client","When possible, build clients via RegisterClient/NewClient's own URL handling (createTransport) instead of hand-set BaseURL"],"exampleFix":"// before\napiURL, _ := url.Parse(\"http://localhost:8080\")\nclient := NewClient(&Config{URL: apiURL, ...})\n// after\napiURL, _ := url.Parse(\"http://localhost:8080/\")\nif !strings.HasSuffix(apiURL.Path, \"/\") {\n    apiURL.Path += \"/\"\n}\nclient := NewClient(&Config{URL: apiURL, ...})","handlingStrategy":"validation","validationCode":"func ensureTrailingSlash(u *url.URL) (*url.URL, error) {\n    if !strings.HasSuffix(u.Path, \"/\") {\n        u.Path += \"/\"\n    }\n    if u.Host == \"\" {\n        return nil, fmt.Errorf(\"BaseURL %q has no host\", u)\n    }\n    return u, nil\n}","typeGuard":"func baseURLIsValid(u *url.URL) bool {\n    return u != nil && u.Host != \"\" && strings.HasSuffix(u.Path, \"/\")\n}","tryCatchPattern":"req, err := client.PrepareRequest(ctx, http.MethodPost, endpoint, body)\nif err != nil {\n    if strings.Contains(err.Error(), \"trailing slash\") {\n        return fmt.Errorf(\"misconfigured client BaseURL (needs trailing slash): %w\", err)\n    }\n    return err\n}","preventionTips":["Always normalize URLs with a trailing slash before building an ApiClient","Prefer NewClient/RegisterClient's URL handling over hand-set BaseURL","Add a unit test asserting BaseURL normalization in your tooling"],"tags":["url","api-client","configuration"],"backgroundTag":"invalid-url-format","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}