caddyserver/caddy · error

cannot specify both a substring search and a regular express

Error message

cannot specify both a substring search and a regular expression search for field '%s'

What it means

Returned by HeaderOps.validate (called during provisioning of header ops): a single replacement entry under header > replace declared both `search` (plain substring) and `search_regexp`. The replacement is either substring or regex, never both; declaring both is rejected so behavior stays unambiguous.

Source

Thrown at modules/caddyhttp/headers/headers.go:180

// containsPlaceholders checks if the string contains Caddy placeholder syntax {key}
func containsPlaceholders(s string) bool {
	_, after, ok := strings.Cut(s, "{")
	if !ok {
		return false
	}
	closeIdx := strings.Index(after, "}")
	if closeIdx == -1 {
		return false
	}
	// Make sure there is content between the brackets
	return closeIdx > 0
}

func (ops HeaderOps) validate() error {
	for fieldName, replacements := range ops.Replace {
		for _, r := range replacements {
			if r.Search != "" && r.SearchRegexp != "" {
				return fmt.Errorf("cannot specify both a substring search and a regular expression search for field '%s'", fieldName)
			}
		}
	}
	return nil
}

// Replacement describes a string replacement,
// either a simple and fast substring search
// or a slower but more powerful regex search.
type Replacement struct {
	// The substring to search for.
	Search string `json:"search,omitempty"`

	// The regular expression to search with.
	SearchRegexp string `json:"search_regexp,omitempty"`

	// The string with which to replace matches.
	Replace string `json:"replace,omitempty"`

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Delete one of the two keys — keep `search` for literal substring matching or `search_regexp` for regex.
  2. If you need both behaviors, create two separate replacement entries in the field's array.
  3. Run `caddy validate --adapter json --config caddy.json` to confirm.

Example fix

// before
"replace": { "Cache-Control": [ { "search": "max-age", "search_regexp": "max-age=\\d+" } ] }
// after
"replace": { "Cache-Control": [ { "search_regexp": "max-age=\\d+" } ] }
Defensive patterns

Strategy: type-guard

Validate before calling

// JSON-level check: a replacement must set exactly one of search / search_regexp
for field, reps := range cfg.Replace {
    for i, r := range reps {
        if r.Search != "" && r.SearchRegexp != "" {
            return fmt.Errorf("field %s replacement %d: set search or search_regexp, not both", field, i)
        }
    }
}

Type guard

// Structural guard for replacement objects (as a Go type constraint on your config loader)
type Replacement struct {
    Search      string `json:"search,omitempty"`
    SearchRegexp string `json:"search_regexp,omitempty"`
}
func (r Replacement) valid() bool { return (r.Search != "") != (r.SearchRegexp != "") }

Prevention

When it happens

Trigger: A JSON config where a replacement object contains both "search" and "search_regexp" keys with non-empty values, e.g. {"search": "foo", "search_regexp": "f(o+)"}.

Common situations: Hand-editing JSON while migrating from substring to regex and leaving the old key; merging configs where both fields accumulate; Caddyfile users rarely hit this since its syntax produces one or the other.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/391ef0c4350ef5e6. Report an issue: GitHub.