JuliusBrussee/caveman · error · ErrInvalidRule

%w: rule %q matches the empty string

Error message

%w: rule %q matches the empty string

What it means

The compiled pattern matches the empty string (re.MatchString("") is true — e.g. `.*`, `a*`, `\s*`, `(a|)`). Applied over the whole body with literal replacement, such a pattern matches at every offset and would collapse the entire capture into a wall of placeholders — a self-inflicted total data loss. Compilation refuses it with ErrInvalidRule, naming the rule. Only this and other 'unusable' shapes are rejected; merely greedy-but-bounded patterns are allowed.

Source

Thrown at shared/platform/redact/payload.go:592

		default:
			return nil, "", fmt.Errorf("%w: %q (rule %q)", ErrRuleUnsupported, r.Type, r.Name)
		}

		if strings.TrimSpace(r.Name) == "" {
			return nil, "", fmt.Errorf("%w: empty name", ErrInvalidRule)
		}
		if r.Pattern == "" {
			return nil, "", fmt.Errorf("%w: rule %q has an empty pattern", ErrInvalidRule, r.Name)
		}
		re, err := regexp.Compile(r.Pattern)
		if err != nil {
			return nil, "", fmt.Errorf("%w: rule %q: %s", ErrInvalidRule, r.Name, err)
		}
		if re.MatchString("") {
			// Such a pattern matches at every position and would replace the
			// whole body with placeholders. Refuse it rather than destroy the
			// capture.
			return nil, "", fmt.Errorf("%w: rule %q matches the empty string", ErrInvalidRule, r.Name)
		}
		repl := r.Replacement
		if repl == "" {
			repl = "[REDACTED:" + r.Name + "]"
		}
		out = append(out, compiledRule{
			name:                 r.Name,
			origin:               OriginOrg,
			re:                   re,
			replIntroducesNeedle: introducesNeedle([]byte(repl)),
			// The replacement is operator-supplied data, not a regexp
			// template: a literal replace keeps "$1" from expanding a captured
			// group back into the output.
			repl: []byte(repl),
		})
	}
	return out, sb.String(), nil
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Change the quantifier so at least one character is required: `.+` instead of `.*`, `\d+` instead of `\d*`.
  2. Test every rule with re.MatchString("") in a rules unit test — this is exactly the package's own guard, replicated cheaply at authoring time.
  3. Add a lint rule in the rule-editor UI flagging unanchored empty-matching patterns.

Example fix

// before
{Name: "free-text", Type: redact.RuleTypeRegex, Pattern: `.*`} // matches empty -> rejected

// after
{Name: "free-text", Type: redact.RuleTypeRegex, Pattern: `.+`} // requires at least one character
Defensive patterns

Strategy: validation

Validate before calling

func noEmptyMatches(rules []redact.Rule) error {
    for _, r := range rules {
        if r.Type != redact.RuleTypeRegex { continue }
        re, err := regexp.Compile(r.Pattern)
        if err != nil { return fmt.Errorf("rule %q: %w", r.Name, err) }
        if re.MatchString("") {
            return fmt.Errorf("rule %q: pattern matches empty string", r.Name)
        }
    }
    return nil
}

Type guard

func matchesNonEmptyOnly(p string) bool {
    re, err := regexp.Compile(p)
    return err == nil && !re.MatchString("")
}

Try / catch

if _, _, err := redact.Payload(body, rules); err != nil {
    if errors.Is(err, redact.ErrInvalidRule) && strings.Contains(err.Error(), "matches the empty string") {
        // change '*' quantifiers to '+' for that rule and revalidate the whole set
    }
}

Prevention

When it happens

Trigger: Any rule whose regex can match zero characters: `.*`, `[A-Z]*`, `\w*`, optional groups that allow empty. The check runs at compile time, so Payload fails on first use with any body.

Common situations: Using '*' where '+' was intended (e.g. `\d*` for digit runs); patterns designed for search contexts where empty matches are harmless; testing the rule with a non-empty sample where the empty match is invisible.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/dffe919e12344fee. Report an issue: GitHub.