grpc/grpc-go · critical
metadata: FromOutgoingContext got an odd number of input…
Error message
metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d
What it means
metadata.FromOutgoingContext (metadata/metadata.go:331) reads the rawMD stored in the context and, while merging the previously-appended kv slices (raw.added), panics at line 351-353 if any added entry has an odd length. Because AppendToOutgoingContext already validates parity when appending, this is a defensive check that fires only when the context's rawMD.added is malformed - i.e. someone constructed or mutated the mdOutgoingKey context value directly instead of using the public API.
Solutions
- Always build outgoing metadata with metadata.NewOutgoingContext and metadata.AppendToOutgoingContext; never write the internal rawMD value yourself.
- If you use context.WithValue for unrelated data, use your own key types to avoid colliding with grpc's internal keys.
- Upgrade grpc-go if you suspect an internal regression; report the bug with a reproducer.
Example fix
// before (forging internal context value -> malformed rawMD.added)
ctx = context.WithValue(ctx, mdOutgoingKey{}, rawMD{added: [][]string{{"only-a-key"}}})
md, _ := metadata.FromOutgoingContext(ctx) // panic: odd
// after
ctx = metadata.AppendToOutgoingContext(ctx, "key", "value")
md, _ := metadata.FromOutgoingContext(ctx) Defensive patterns
Strategy: validation
Validate before calling
// Do not forge the internal rawMD value. Always use the public API: ctx = metadata.NewOutgoingContext(ctx, md) ctx = metadata.AppendToOutgoingContext(ctx, "k", "v") md, ok := metadata.FromOutgoingContext(ctx) // safe
Prevention
- Never write the unexported mdOutgoingKey context value directly.
- Use your own typed context keys for non-gRPC data to avoid collisions.
- Upgrade grpc-go if you suspect an internal regression and report with a reproducer.
When it happens
Trigger: Manually inserting a rawMD into the context under the unexported mdOutgoingKey with a corrupted (odd-length) added slice; an internal/grpc-Go bug that mis-manipulates raw.added; reflection-based test code that tampers with the context value.
Common situations: Bypassing the public metadata API (e.g. using context.WithValue with an internal key) to forge outgoing metadata; a grpc-internal regression; instrumentation/mocking that rewrites the context value.
Related errors
- metadata: AppendToOutgoingContext got an odd number of…
- metadata: Pairs got the odd number of input pairs for…
- buffer size is not an exponent of two
- Cannot free freed buffer
- cannot have a leading slash
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/fa889807a4657825.
Report an issue: GitHub.
Appendix: source
Thrown at metadata/metadata.go:352
return nil, false
}
mdSize := len(raw.md)
for i := range raw.added {
mdSize += len(raw.added[i]) / 2
}
out := make(MD, mdSize)
for k, v := range raw.md {
// We need to manually convert all keys to lower case, because MD is a
// map, and there's no guarantee that the MD attached to the context is
// created using our helper functions.
key := strings.ToLower(k)
out[key] = copyOf(v)
}
for _, added := range raw.added {
if len(added)%2 == 1 {
panic(fmt.Sprintf("metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d", len(added)))
}
for i := 0; i < len(added); i += 2 {
key := strings.ToLower(added[i])
out[key] = append(out[key], added[i+1])
}
}
return out, ok
}
type rawMD struct {
md MD
added [][]string
}
View on GitHub (pinned to 0c51461d27)