crowdsecurity/crowdsec · error · InvalidFilter

invalid contains value: %w: %w

Error message

invalid contains value: %w: %w

What it means

ExpireDecisionsWithFilter accepts a filter map; when the 'contains' key is present its value must parse as a Go bool via strconv.ParseBool. If the value is not one of 1/t/T/true/TRUE/True/0/f/F/false/FALSE/False, the bool conversion fails and the error is wrapped together with the InvalidFilter sentinel so callers can classify it as a bad filter.

Source

Thrown at pkg/database/decisions.go:236

// ExpireDecisionsWithFilter updates the expiration time to now() for the decisions matching the filter, and returns the updated items
func (c *Client) ExpireDecisionsWithFilter(ctx context.Context, filter map[string][]string) (int, []*ent.Decision, error) {
	var (
		err error
		rng csnet.Range
	)

	contains := true
	// if contains is true, return bans that *contains* the given value (value is the inner)
	// else, return bans that are *contained* by the given value (value is the outer)
	decisions := c.Ent.Decision.Query().Where(decision.UntilGT(time.Now().UTC()))

	for param, value := range filter {
		switch param {
		case "contains":
			contains, err = strconv.ParseBool(value[0])
			if err != nil {
				return 0, nil, fmt.Errorf("invalid contains value: %w: %w", err, InvalidFilter)
			}
		case "scopes":
			decisions = decisions.Where(decision.ScopeEQ(value[0]))
		case "uuid":
			decisions = decisions.Where(decision.UUIDIn(value...))
		case "origin":
			decisions = decisions.Where(decision.OriginEQ(value[0]))
		case "value":
			decisions = decisions.Where(decision.ValueEQ(value[0]))
		case "type":
			decisions = decisions.Where(decision.TypeEQ(value[0]))
		case "ip", "range":
			rng, err = csnet.NewRange(value[0])
			if err != nil {
				return 0, nil, fmt.Errorf("unable to convert '%s' to int: %w: %w", value[0], err, InvalidIPOrRange)
			}
		case "scenario":
			decisions = decisions.Where(decision.ScenarioEQ(value[0]))

View on GitHub (pinned to 909b515798)

Solutions

  1. Pass a value accepted by strconv.ParseBool: 1, t, T, true, TRUE, True, 0, f, F, false, FALSE, False
  2. Fix the calling code to serialize booleans properly instead of formatting them as yes/no or on/off
  3. Trim whitespace/quotes from the value before building the filter map
  4. Check errors.Is(err, database.InvalidFilter) in the caller to surface a 400-style message to the user

Example fix

// before
c.ExpireDecisionsWithFilter(ctx, map[string][]string{"contains": {"yes"}})
// after
c.ExpireDecisionsWithFilter(ctx, map[string][]string{"contains": {"true"}})
Defensive patterns

Strategy: validation

Validate before calling

func validContains(v string) bool {
    _, err := strconv.ParseBool(v)
    return err == nil
}
if !validContains(filterVal) { return fmt.Errorf("contains must be a bool, got %q", filterVal) }

Try / catch

if _, _, err := client.ExpireDecisionsWithFilter(ctx, filter); err != nil {
    if errors.Is(err, database.InvalidFilter) {
        return fmt.Errorf("bad filter: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ExpireDecisionsWithFilter (directly or via HandleDeletedDecisionsV3, DeleteDecisions, or the cscli decisions CLI) with filter["contains"] set to a non-boolean string such as "yes", "on", "1 ", or "true1".

Common situations: CLI users passing --contains=yes instead of --contains=true; JSON/API clients sending human-style booleans; automation scripts quoting the value wrongly so it arrives as "'true'".

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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