crowdsecurity/crowdsec · error
BaseURL must have a trailing slash, but %q does not
Error message
BaseURL must have a trailing slash, but %q does not
What it means
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.
Source
Thrown at pkg/apiclient/client_http.go:23
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strings"
log "github.com/sirupsen/logrus"
)
const compressionMinSize = 5 * 1024 // 5KB
func (c *ApiClient) PrepareRequest(ctx context.Context, method, url string, body any) (*http.Request, error) {
if !strings.HasSuffix(c.BaseURL.Path, "/") {
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
}
u, err := c.BaseURL.Parse(url)
if err != nil {
return nil, err
}
var buf io.ReadWriter
compressedBody := false
if body != nil {
jsonBuf := &bytes.Buffer{}
enc := json.NewEncoder(jsonBuf)
enc.SetEscapeHTML(false)
if err = enc.Encode(body); err != nil {
return nil, err
}View on GitHub (pinned to 909b515798)
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
Example fix
// before
apiURL, _ := url.Parse("http://localhost:8080")
client := NewClient(&Config{URL: apiURL, ...})
// after
apiURL, _ := url.Parse("http://localhost:8080/")
if !strings.HasSuffix(apiURL.Path, "/") {
apiURL.Path += "/"
}
client := NewClient(&Config{URL: apiURL, ...}) Defensive patterns
Strategy: validation
Validate before calling
func ensureTrailingSlash(u *url.URL) (*url.URL, error) {
if !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
}
if u.Host == "" {
return nil, fmt.Errorf("BaseURL %q has no host", u)
}
return u, nil
} Type guard
func baseURLIsValid(u *url.URL) bool {
return u != nil && u.Host != "" && strings.HasSuffix(u.Path, "/")
} Try / catch
req, err := client.PrepareRequest(ctx, http.MethodPost, endpoint, body)
if err != nil {
if strings.Contains(err.Error(), "trailing slash") {
return fmt.Errorf("misconfigured client BaseURL (needs trailing slash): %w", err)
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- missing lapi client credentials
- no appsec_config provided
- missing TLS key file
- missing TLS cert file
- group_name is mandatory for CloudwatchSource
AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06).
Data as JSON: /api/errors/44641767081d63d9.
Report an issue: GitHub.