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 nilView on GitHub (pinned to 03255a9237)
Solutions
- Lowercase all metadata keys with strings.ToLower before adding them.
- Replace any disallowed characters; keep keys to [a-z0-9-_.] only.
- 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
- Always lowercase header keys (HTTP/2 requires it).
- Run metadata.Validate(md) at the boundary where headers enter your code.
- Reject mixed-case input from config files early.
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
- there is an empty key in the header
- header key %q contains value with non-printable ASCII charac
- no error details for status with code OK
- duplicated name
- "headers" %d: unsupported "key" %s
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/f15b95386a47c8e8.
Report an issue: GitHub.