grpc/grpc-go · critical
metadata: FromOutgoingContext got an odd number of input pai
Error message
metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d
What it means
metadata.FromOutgoingContext (metadata.go:331) reads outgoing metadata back out of a context. It panics when one of the internally-stored appended slices has an odd length (the len(added)%2==1 check at line 351). Unlike Pairs/AppendToOutgoingContext, this is a READ-time panic: it indicates the context's outgoing metadata was corrupted after the fact, not that the FromOutgoingContext caller passed odd arguments (it takes none).
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 03255a9237)
Solutions
- Audit any code that writes to the mdOutgoingKey context value directly and route all metadata through the public AppendToOutgoingContext / NewOutgoingContext APIs.
- Ensure a single consistent grpc-go version across modules (go mod tidy / check for duplicates with 'go list -m all | grep grpc').
- If using a grpc fork, update it to match upstream AppendToOutgoingContext behavior.
Example fix
// before: a test helper pokes the context value directly with an odd slice
ctx = context.WithValue(ctx, mdOutgoingKey{}, rawMD{added: [][]string{{"only-key"}}})
out, _ := metadata.FromOutgoingContext(ctx) // panics at read time
// after: always use the public API so parity is guaranteed
ctx = metadata.AppendToOutgoingContext(ctx, "key", "value")
out, _ := metadata.FromOutgoingContext(ctx) Defensive patterns
Strategy: validation
Validate before calling
// FromOutgoingContext panics on internally-corrupted data.
// Prevent it by NEVER writing the mdOutgoingKey context value directly;
// only use metadata.NewOutgoingContext / AppendToOutgoingContext.
// If you must read untrusted contexts, recover defensively:
func safeFromOutgoing(ctx context.Context) (md metadata.MD, ok bool) {
defer func() { _ = recover() }() // last resort only
return metadata.FromOutgoingContext(ctx)
} Prevention
- Never inject or mutate the mdOutgoingKey context value via reflection or copied internal structs.
- Keep a single grpc-go version across all modules to avoid rawMD layout mismatches.
- Audit custom context wrappers and test helpers that touch outgoing metadata.
When it happens
Trigger: FromOutgoingContext(ctx) panics because rawMD.added (populated via AppendToOutgoingContext) contains an odd-length slice. This should be impossible through the public API; it happens when something manipulates the context value directly (reflection, an internal/grpcx helper, an older-incompatible grpc version, or a buggy custom context wrapper) and writes a malformed rawMD.
Common situations: A custom middleware or test helper that injects metadata into the context via reflection or a copied internal struct; vendoring mismatched grpc versions where the rawMD layout changed; a fork of grpc with a bug in AppendToOutgoingContext.
Related errors
- metadata: AppendToOutgoingContext got an odd number of input
- metadata: Pairs got the odd number of input pairs for metada
- The unary server interceptor was already set and may not be
- The stream server interceptor was already set and may not be
- The tap handle was already set and may not be reset.
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/fa889807a4657825.
Report an issue: GitHub.