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

  1. Ensure the URL used to build the client ends with '/', e.g. 'http://localhost:8080/'
  2. Normalize in code: if !strings.HasSuffix(u.Path, "/") { u.Path += "/" } before constructing the client
  3. 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

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


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/44641767081d63d9. Report an issue: GitHub.