crowdsecurity/crowdsec · error

invalid URL template '%s'

Error message

invalid URL template '%s'

What it means

The hub Downloader builds download URLs from a template (URLTemplate) that must contain exactly two '%s' placeholders, filled with the branch and the remote path. urlTo validates this by re-sprintfing with literal '%s' values; if the template doesn't have the two placeholders, it returns 'invalid URL template' and FetchIndex/FetchContent fail.

Source

Thrown at pkg/cwhub/download.go:39

	Branch      string
	URLTemplate string
}

// IndexProvider retrieves and writes .index.json
type IndexProvider interface {
	FetchIndex(ctx context.Context, indexFile string, withContent bool, logger *logrus.Logger) (bool, error)
}

// ContentProvider retrieves and writes the YAML files with the item content.
type ContentProvider interface {
	FetchContent(ctx context.Context, remotePath, destPath, wantHash string, logger *logrus.Logger) (bool, string, error)
}

// urlTo builds the URL to download a file from the remote hub.
func (d *Downloader) urlTo(remotePath string) (*url.URL, error) {
	// the template must contain two string placeholders
	if fmt.Sprintf(d.URLTemplate, "%s", "%s") != d.URLTemplate {
		return nil, fmt.Errorf("invalid URL template '%s'", d.URLTemplate)
	}

	raw := fmt.Sprintf(d.URLTemplate, d.Branch, remotePath)

	parsed, err := url.Parse(raw)
	if err != nil {
		return nil, fmt.Errorf("failed to parse URL: %w", err)
	}

	return parsed, nil
}

// FetchIndex downloads the index from the hub and writes it to the filesystem.
// It uses a temporary file to avoid partial downloads, and won't overwrite the original
// if it has not changed.
// Return true if the file has been updated, false if already up to date.
func (d *Downloader) FetchIndex(ctx context.Context, destPath string, withContent bool, logger *logrus.Logger) (downloaded bool, err error) {
	url, err := d.urlTo(".index.json")

View on GitHub (pinned to 909b515798)

Solutions

  1. Ensure the template contains exactly two %s placeholders, e.g. 'https://example.com/hub/%s/%s'.
  2. Restore the default hub URL in config.yaml (api.server.hub_url) if you overrode it.
  3. Verify the branch substitution is done by the downloader, not baked into the URL.
  4. After fixing, run `cscli hub update` / `cscli hub upgrade` to re-fetch the index.

Example fix

// before
URLTemplate = "https://hub.example.com/crowdsec/master/" // hard-coded branch
// after
URLTemplate = "https://hub.example.com/crowdsec/%s/%s"
Defensive patterns

Strategy: validation

Validate before calling

if strings.Count(template, "%s") != 2 {
    return fmt.Errorf("hub URL template must contain exactly two %%s placeholders: %q", template)
}

Try / catch

u, err := d.urlTo(remotePath)
if err != nil {
    return fmt.Errorf("cannot build hub download URL: %w", err)
}

Prevention

When it happens

Trigger: Constructing a cwhub Downloader with a URLTemplate lacking two '%s' placeholders (e.g. only one placeholder, hard-coded branch, or using %v/%d instead of %s).

Common situations: Custom hub configuration (hub_url / api URL in config.yaml) pointing to a mirror whose URL format differs from the expected `<base>/<branch>/<path>` scheme; typos when overriding the hub URL; older config formats after an upgrade.

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/8c8d597d0117e633. Report an issue: GitHub.