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

  1. Always build outgoing metadata with metadata.NewOutgoingContext and metadata.AppendToOutgoingContext; never write the internal rawMD value yourself.
  2. If you use context.WithValue for unrelated data, use your own key types to avoid colliding with grpc's internal keys.
  3. 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

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


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)