caddyserver/caddy · error
too many unclosed placeholders
Error message
too many unclosed placeholders
What it means
The core replacer (used to expand {placeholders}) aborts when it encounters more than 100 open braces with no matching close brace in the input. This guards against catastrophic quadratic scanning on pathological input (issue #4170) — a flood of '{' characters would otherwise make replacement extremely slow.
Source
Thrown at replacer.go:212
scan:
for i := 0; i < len(input); i++ {
// check for escaped braces
if i > 0 && input[i-1] == phEscape && (input[i] == phClose || input[i] == phOpen) {
sb.WriteString(input[lastWriteCursor : i-1])
lastWriteCursor = i
continue
}
if input[i] != phOpen {
continue
}
// our iterator is now on an unescaped open brace (start of placeholder)
// too many unclosed placeholders in absolutely ridiculous input can be extremely slow (issue #4170)
if unclosedCount > 100 {
return "", fmt.Errorf("too many unclosed placeholders")
}
// find the end of the placeholder
end := strings.Index(input[i:], string(phClose)) + i
if end < i {
unclosedCount++
continue
}
// if necessary look for the first closing brace that is not escaped
for end > 0 && end < len(input)-1 && input[end-1] == phEscape {
nextEnd := strings.Index(input[end+1:], string(phClose))
if nextEnd < 0 {
unclosedCount++
continue scan
}
end += nextEnd + 1
}View on GitHub (pinned to 50e54ee279)
Solutions
- Escape literal braces with a backslash (\{ and \}) so they are not treated as placeholder opens
- Sanitize user-supplied input before feeding it into replaced values (strip or escape '{')
- Upgrade Caddy if on an old version predating the fix for #4170
Example fix
// before
tpl := "data: " + userInput // userInput has 100+ '{'
val := repl.ReplaceKnown(tpl, "")
// after
import "strings"
tpl := "data: " + strings.ReplaceAll(strings.ReplaceAll(userInput, "{", "\\{"), "}", "\\}")
val := repl.ReplaceKnown(tpl, "") Defensive patterns
Strategy: validation
Validate before calling
if strings.Count(input, "{")-strings.Count(input, "}") > 100 {
return fmt.Errorf("input has too many unmatched open braces")
} Try / catch
if out, err := repl.ReplaceOrErr(input, false, false); err != nil {
if strings.Contains(err.Error(), "too many unclosed placeholders") {
out = sanitizeBraces(input) // escape braces and retry
}
} Prevention
- Escape literal braces as \{ and \} in replaced strings
- Never feed untrusted raw text into placeholder-substituted fields
When it happens
Trigger: Calling repl.Replace()/ReplaceOrErr (directly or via any config field that goes through the replacer) on input containing 100+ unmatched '{' characters; e.g. logging or proxying a header value full of braces into a replaced field.
Common situations: Client-controlled data (headers, paths, bodies) flowing into placeholder-substituted config values; templating code that concatenates user input with literal braces; JSON snippets passed through the replacer.
Related errors
- unrecognized placeholder %s%s%s
- evaluated placeholder %s%s%s is empty
- replacing listen address: %v
- server %s, listener %d: %v
- %s: parsing listen address '%s': %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/12f9ee73c72eca79.
Report an issue: GitHub.