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
- Delete one of the two keys — keep `search` for literal substring matching or `search_regexp` for regex.
- If you need both behaviors, create two separate replacement entries in the field's array.
- 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 migrating a replacement from substring to regex, delete the old key in the same commit.
- Prefer the Caddyfile syntax (replace/a/replace_b) which cannot express both.
- Lint JSON configs for replacement objects containing both keys.
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
- replacement %d for header field '%s': %v
- %s: entry %q contains non-ASCII characters
- %s: entry %q has '*' in an invalid position (only a trailing
- %s: entry %q does not contain a %q
- missing 'req' argument
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/391ef0c4350ef5e6.
Report an issue: GitHub.