caddyserver/caddy · error

unsupported map key type in header match: %T

Error message

unsupported map key type in header match: %T

What it means

Thrown while converting a CEL-evaluated map into map[string]any for a header match in the CEL matcher. The expression produced a map whose key type is not a Go string (e.g. an int or bool key after ConvertToNative to map[any]any), so the key cannot be used as an HTTP header name. Caddy rejects it because HTTP header names must be strings.

Source

Thrown at modules/caddyhttp/celmatcher.go:686

// of string.
func CELValueToMapStrList(data ref.Val) (map[string][]string, error) {
	// Prefer map[string]any, but newer cel-go versions may return map[any]any
	mapStrType := reflect.TypeFor[map[string]any]()
	mapStrRaw, err := data.ConvertToNative(mapStrType)
	var mapStrIface map[string]any
	if err != nil {
		// Try map[any]any and convert keys to strings
		mapAnyType := reflect.TypeFor[map[any]any]()
		mapAnyRaw, err2 := data.ConvertToNative(mapAnyType)
		if err2 != nil {
			return nil, err
		}
		mapAnyIface := mapAnyRaw.(map[any]any)
		mapStrIface = make(map[string]any, len(mapAnyIface))
		for k, v := range mapAnyIface {
			ks, ok := k.(string)
			if !ok {
				return nil, fmt.Errorf("unsupported map key type in header match: %T", k)
			}
			mapStrIface[ks] = v
		}
	} else {
		mapStrIface = mapStrRaw.(map[string]any)
	}
	mapStrListStr := make(map[string][]string, len(mapStrIface))
	for k, v := range mapStrIface {
		switch val := v.(type) {
		case string:
			mapStrListStr[k] = []string{val}
		case types.String:
			mapStrListStr[k] = []string{string(val)}
		case []string:
			mapStrListStr[k] = val
		case []ref.Val:
			convVals := make([]string, len(val))
			for i, elem := range val {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Make every key in the matcher's map a string literal (quote keys: {"Accept": ["text/html"]})
  2. If generating config from code, serialize maps with string keys only (use map[string][]string in Go)
  3. Check the CEL expression for computed map constructions and replace them with literal string keys

Example fix

// before
{"expression": "{1: 'v'}.all(k, k)"}
// after
{"expression": "{'accept': 'v'}['accept'] == 'v'"}
Defensive patterns

Strategy: validation

Validate before calling

// Go: validate matcher input maps have string keys before building config
func checkKeysString(m any) error {
    rv := reflect.ValueOf(m)
    if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String {
        return fmt.Errorf("map keys must be strings, got %v", rv.Type())
    }
    return nil
}

Prevention

When it happens

Trigger: Writing a CEL expression matcher whose header-map input is built from a map literal with non-string keys, e.g. {header_match: {1: 'v'}} or a map produced by a CEL function returning non-string keys; the code path first tries ConvertToNative(map[string]any), fails, retries map[any]any, and hits a non-string key.

Common situations: Hand-written JSON configs with CEL matchers where map keys were accidentally quoted as numbers/booleans; programmatically generated matcher configs from YAML/JSON tooling that types header names as non-strings.

Related errors


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