grpc/grpc-go · error
there is an empty key in the header
Error message
there is an empty key in the header
What it means
gRPC metadata keys must be non-empty. metadata.ValidateKey at metadata.go:106-107 rejects an empty key string with this error. Pseudo-headers (starting with ':') are allowed and short-circuit before this check, so the empty key specifically means a genuinely zero-length key name in an outbound metadata.MD.
Solutions
- Audit the metadata.Pairs / metadata.Pairs / metadata.NewOutgoingContext call site that produced the MD and find the empty key.
- Ensure any variable used as a key is non-empty before insertion (guard with an explicit check).
- If the key is optional, skip adding it when the value is empty rather than inserting an empty key.
- Add a unit test that calls metadata.Validate on your constructed MD.
Example fix
// before:
// md := metadata.Pairs(authHeader, token, "", "oops")
// ctx = metadata.NewOutgoingContext(ctx, md)
// after:
// md := metadata.Pairs(authHeader, token)
// if extraKey != "" {
// md.Append(extraKey, extraVal)
// }
// ctx = metadata.NewOutgoingContext(ctx, md) Defensive patterns
Strategy: validation
Validate before calling
package main
import (
"fmt"
"google.golang.org/grpc/metadata"
)
func buildOutgoingMD(pairs []string) (metadata.MD, error) {
md := metadata.MD{}
for i := 0; i+1 < len(pairs); i += 2 {
k := pairs[i]
if k == "" {
return nil, fmt.Errorf("refusing to add metadata with empty key")
}
md.Append(k, pairs[i+1])
}
return md, nil
}
// func main() { _, _ = buildOutgoingMD(nil) } Try / catch
// gRPC rejects invalid metadata before sending; the RPC fails with an error.
// Validate before attaching so the call never reaches that path.
//
// md, err := buildOutgoingMD(pairs)
// if err != nil { return err }
// ctx = metadata.NewOutgoingContext(ctx, md) Prevention
- Never derive a metadata key from a variable that may be empty without checking.
- Wrap all metadata construction in a helper that rejects empty keys.
- Call metadata.Validate(md) in unit tests on your produced MD.
When it happens
Trigger: Triggered when metadata.Validate or metadata.ValidatePair iterates a metadata.MD whose map contains the key "" (empty string), or when a caller appends metadata.Pairs("") / metadata.Pairs("", "value"). The validation runs before serializing metadata onto the wire.
Common situations: Dynamic header construction sets a key from a variable that resolved to empty (e.g. unset env var or empty trace/context-propagation key), metadata.Pairs("", token) was written by mistake, or a header normalization function stripped/emptied a key.
Related errors
- header key contains illegal characters not in [0-9a-z-_.]
- header key contains value with non-printable ASCII…
- invalid requestHashHeader
- invalid requestHashHeader
- buffer size is not an exponent of two
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/2e48e30191765bb1.
Report an issue: GitHub.
Appendix: source
Thrown at internal/metadata/metadata.go:107
// hasNotPrintable return true if msg contains any characters which are not in %x20-%x7E
func hasNotPrintable(msg string) bool {
// for i that saving a conversion if not using for range
for i := 0; i < len(msg); i++ {
if msg[i] < 0x20 || msg[i] > 0x7E {
return true
}
}
return false
}
// 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.View on GitHub (pinned to 0c51461d27)