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

  1. Escape literal braces with a backslash (\{ and \}) so they are not treated as placeholder opens
  2. Sanitize user-supplied input before feeding it into replaced values (strip or escape '{')
  3. 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

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


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