grpc/grpc-go · error

header key %q contains illegal characters not in [0-9a-z-_.]

Error message

header key %q contains illegal characters not in [0-9a-z-_.]

What it means

Returned by metadata.ValidateKey when a key contains any byte outside [0-9a-z-_.] (after the pseudo-header ':' check). Notably uppercase A-Z are rejected: gRPC requires lowercase keys. This protects against invalid HTTP/2 header names being sent on the wire.

Source

Thrown at internal/metadata/metadata.go:117

// ValidateKey validates a key with the following rules (pseudo-headers are
// skipped):
// - the key must contain one or more characters.
// - the characters in the key must be in [0-9 a-z _ - .].
func ValidateKey(key string) error {
	// key should not be empty
	if key == "" {
		return fmt.Errorf("there is an empty key in the header")
	}
	// pseudo-header will be ignored
	if key[0] == ':' {
		return nil
	}
	// check key, for i that saving a conversion if not using for range
	for i := 0; i < len(key); i++ {
		r := key[i]
		if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '.' && r != '-' && r != '_' {
			return fmt.Errorf("header key %q contains illegal characters not in [0-9a-z-_.]", key)
		}
	}
	return nil
}

// ValidatePair validates a key-value pair with the following rules
// (pseudo-header are skipped):
//   - the key must contain one or more characters.
//   - the characters in the key must be in [0-9 a-z _ - .].
//   - if the key ends with a "-bin" suffix, no validation of the corresponding
//     value is performed.
//   - the characters in every value must be printable (in [%x20-%x7E]).
func ValidatePair(key string, vals ...string) error {
	if err := ValidateKey(key); err != nil {
		return err
	}
	if strings.HasSuffix(key, "-bin") {
		return nil

View on GitHub (pinned to 03255a9237)

Solutions

  1. Lowercase all metadata keys with strings.ToLower before adding them.
  2. Replace any disallowed characters; keep keys to [a-z0-9-_.] only.
  3. Run metadata.Validate(md) once at construction time to fail fast.

Example fix

// before
md := metadata.Pairs("Authorization", token) // uppercase rejected
// after
md := metadata.Pairs(strings.ToLower("Authorization"), token) // "authorization"
Defensive patterns

Strategy: validation

Validate before calling

// Lowercase and constrain keys before adding to metadata.
func safeKey(k string) (string, error) {
    k = strings.ToLower(k)
    for i := 0; i < len(k); i++ {
        c := k[i]
        if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') {
            return "", fmt.Errorf("bad header key %q", k)
        }
    }
    return k, nil
}

Type guard

func isValidKey(k string) bool {
    if k == "" || k[0] == ':' { return k != "" }
    for i := 0; i < len(k); i++ {
        c := k[i]
        if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') {
            return false
        }
    }
    return true
}

Try / catch

if err := metadata.Validate(md); err != nil {
    // auto-repair by lowercasing keys, then retry
    md = lowercaseKeys(md)
}

Prevention

When it happens

Trigger: Passing a key like "Authorization", "Content-Type", "X-Trace-Id", or any key containing spaces, colons, or symbols. Keys derived from inbound HTTP/1 headers without lowercasing.

Common situations: Reusing HTTP/1 header names verbatim (capitalized); copying header keys from JSON config that preserves case; interop with proxies that inject mixed-case headers.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/f15b95386a47c8e8. Report an issue: GitHub.