grpc/grpc-go · critical
metadata: AppendToOutgoingContext got an odd number of input
Error message
metadata: AppendToOutgoingContext got an odd number of input pairs for metadata: %d
What it means
metadata.AppendToOutgoingContext (metadata.go:250) appends key/value pairs to the outgoing metadata already attached to a context. Like Pairs, it panics on an odd argument count because each key must be followed by exactly one value. This is called per-RPC (inside request building), so the panic occurs in the hot path and will crash the serving goroutine.
Source
Thrown at metadata/metadata.go:252
// not be modified after calling this function.
func NewIncomingContext(ctx context.Context, md MD) context.Context {
return context.WithValue(ctx, mdIncomingKey{}, md)
}
// NewOutgoingContext creates a new context with outgoing md attached. If used
// in conjunction with AppendToOutgoingContext, NewOutgoingContext will
// overwrite any previously-appended metadata. md must not be modified after
// calling this function.
func NewOutgoingContext(ctx context.Context, md MD) context.Context {
return context.WithValue(ctx, mdOutgoingKey{}, rawMD{md: md})
}
// AppendToOutgoingContext returns a new context with the provided kv merged
// with any existing metadata in the context. Please refer to the documentation
// of Pairs for a description of kv.
func AppendToOutgoingContext(ctx context.Context, kv ...string) context.Context {
if len(kv)%2 == 1 {
panic(fmt.Sprintf("metadata: AppendToOutgoingContext got an odd number of input pairs for metadata: %d", len(kv)))
}
md, _ := ctx.Value(mdOutgoingKey{}).(rawMD)
added := make([][]string, len(md.added)+1)
copy(added, md.added)
kvCopy := make([]string, 0, len(kv))
for i := 0; i < len(kv); i += 2 {
kvCopy = append(kvCopy, strings.ToLower(kv[i]), kv[i+1])
}
added[len(added)-1] = kvCopy
return context.WithValue(ctx, mdOutgoingKey{}, rawMD{md: md.md, added: added})
}
// FromIncomingContext returns the incoming metadata in ctx if it exists.
//
// All keys in the returned MD are lowercase.
func FromIncomingContext(ctx context.Context) (MD, bool) {
md, ok := ctx.Value(mdIncomingKey{}).(MD)
if !ok {View on GitHub (pinned to 03255a9237)
Solutions
- Locate the AppendToOutgoingContext call in the panic stack trace and ensure arguments are even and paired.
- For conditional headers, always append both the key and the value together inside the same branch.
- Add a unit test that asserts len(args)%2==0 for any dynamic pair slice you build.
Example fix
// before ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token, "caller") // odd // after ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token, "caller", callerID)
Defensive patterns
Strategy: validation
Validate before calling
// Guard dynamic per-RPC metadata before appending
if len(pairs)%2 != 0 {
return ctx, fmt.Errorf("AppendToOutgoingContext needs even pairs, got %d", len(pairs))
}
return metadata.AppendToOutgoingContext(ctx, pairs...), nil Prevention
- Append conditional headers as complete (key,value) units inside one branch.
- Avoid concatenating slices to build the kv list; append in twos.
- Unit-test interceptors that call AppendToOutgoingContext with varying inputs.
When it happens
Trigger: Calling metadata.AppendToOutgoingContext(ctx, kv...) with an odd number of arguments, e.g. AppendToOutgoingContext(ctx, "auth", token, "extra"). The len(kv)%2==1 guard fires immediately.
Common situations: Per-RPC interceptor or handler appending a header but forgetting the value; a conditional append that added the key branch but not the value; refactoring that dropped an argument.
Related errors
- metadata: Pairs got the odd number of input pairs for metada
- metadata: FromOutgoingContext got an odd number of input pai
- cannot register Codec with empty string result for Name()
- cannot register CodecV2 with empty string result for Name()
- there is an empty key in the header
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/e1d74469e1edc335.
Report an issue: GitHub.