grpc/grpc-go · critical
metadata: AppendToOutgoingContext got an odd number of…
Error message
metadata: AppendToOutgoingContext got an odd number of input pairs for metadata: %d
What it means
metadata.AppendToOutgoingContext (metadata/metadata.go:250) appends key/value pairs to the outgoing metadata stored in a context and panics at line 251-253 when len(kv) is odd, for the same reason as Pairs: every key needs a value. The pairs are stored (lowercased keys) and later merged on RPC send.
Solutions
- Pass an even number of string arguments (key, value, key, value, ...).
- Validate len(kv)%2 == 0 before calling, especially when kv is built dynamically.
- Prefer metadata.NewOutgoingContext(ctx, md) with a constructed MD for complex metadata.
Example fix
// before ctx = metadata.AppendToOutgoingContext(ctx, "auth", token, "request-id") // odd -> panic // after ctx = metadata.AppendToOutgoingContext(ctx, "auth", token, "request-id", reqID)
Defensive patterns
Strategy: validation
Validate before calling
func safeAppend(ctx context.Context, kv ...string) context.Context {
if len(kv)%2 == 1 {
return ctx // skip malformed append
}
return metadata.AppendToOutgoingContext(ctx, kv...)
} Prevention
- Pass key/value pairs as an even-length vararg list.
- Validate len(kv)%2 == 0 when kv is assembled dynamically.
- For complex metadata, build an MD and use metadata.NewOutgoingContext.
When it happens
Trigger: Calling metadata.AppendToOutgoingContext(ctx, "key") with an odd argument count; dynamically building the kv varargs and dropping a value; a helper that conditionally appends a key without its value.
Common situations: Building per-RPC metadata with a typo; a conditional that appends a header name but skips its value in some branch; refactoring call sites and losing an argument.
Related errors
- metadata: FromOutgoingContext got an odd number of input…
- metadata: Pairs got the odd number of input pairs for…
- Cannot free freed buffer
- cannot have a leading slash
- cannot have exclude and a '*' wildcard
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/e1d74469e1edc335.
Report an issue: GitHub.
Appendix: 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 0c51461d27)