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

  1. Audit the metadata.Pairs / metadata.Pairs / metadata.NewOutgoingContext call site that produced the MD and find the empty key.
  2. Ensure any variable used as a key is non-empty before insertion (guard with an explicit check).
  3. If the key is optional, skip adding it when the value is empty rather than inserting an empty key.
  4. 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

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


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)