caddyserver/caddy · error

%s: entry %q contains non-ASCII characters

Error message

%s: entry %q contains non-ASCII characters

What it means

provisionHeaderAliasAllowlist validates header-alias allowlist entries (e.g. for the header-alias filter on requests) at server provisioning. It rejects any entry containing a byte >= 0x80 because Go's net/http parser returns 400 for non-ASCII header names, so such an entry could never match a real request. The %s is the directive name ('_' or '.' separated variants) and %q the offending entry.

Source

Thrown at modules/caddyhttp/server.go:394

	allow string // canonical allowed form, e.g. "Webhook_" or "Webhook."
	drop  string // canonical hyphenated form, e.g. "Webhook-"
}

// provisionHeaderAliasAllowlist validates entries for a header-alias
// allowlist (ExpectedUnderscoreHeaders or ExpectedDotHeaders) and builds
// the precomputed maps and prefix rules used by the hot-path filter in
// serveHTTP. sep is the separator the entries must contain ('_' or '.');
// directive is the Caddyfile/JSON name used in error messages.
func provisionHeaderAliasAllowlist(entries []string, sep rune, directive string) (exactAllow, exactDrop map[string]struct{}, prefixRules []aliasPrefixRule, err error) {
	exactAllow = make(map[string]struct{}, len(entries))
	exactDrop = make(map[string]struct{}, len(entries))

	for _, entry := range entries {
		// Reject non-ASCII bytes: Go's HTTP parser returns 400 for
		// non-ASCII header names, so such entries can never match.
		for i := 0; i < len(entry); i++ {
			if entry[i] >= 0x80 {
				return nil, nil, nil, fmt.Errorf("%s: entry %q contains non-ASCII characters", directive, entry)
			}
		}

		isGlob := strings.HasSuffix(entry, "*")
		name := entry
		if isGlob {
			name = strings.TrimSuffix(entry, "*")
		}

		// Reject entries with '*' not at the trailing position.
		if strings.ContainsRune(name, '*') {
			return nil, nil, nil, fmt.Errorf("%s: entry %q has '*' in an invalid position (only a trailing '*' is allowed)", directive, entry)
		}

		// The name (without trailing '*') must contain at least one separator.
		if !strings.ContainsRune(name, sep) {
			return nil, nil, nil, fmt.Errorf("%s: entry %q does not contain a %q", directive, entry, sep)
		}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Rewrite the entry using only ASCII letters, digits, '-', '_' and '.'
  2. Hex-dump the config region to find hidden non-ASCII bytes (e.g. 'grep -P "[\x80-\xff]" config')
  3. Disable editor features that produce smart quotes when editing configs

Example fix

// before
"-": ["X-Custom Header"]  // contains U+00A0 non-breaking space

// after
"-": ["X-Custom_Header"]
Defensive patterns

Strategy: validation

Validate before calling

func allASCII(entries []string) error {
    for _, e := range entries {
        for i := 0; i < len(e); i++ {
            if e[i] >= 0x80 {
                return fmt.Errorf("entry %q has non-ASCII byte at %d", e, i)
            }
        }
    }
    return nil
}

Prevention

When it happens

Trigger: Configuring the header alias allowlist with a header name containing UTF-8 or any high-byte characters, e.g. copied from documentation with a smart quote or non-breaking space.

Common situations: Copy-pasting header names from rich-text docs/websites that insert invisible Unicode characters, or editing configs in editors that save smart quotes.

Related errors


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