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

  1. Audit any code that writes to the mdOutgoingKey context value directly and route all metadata through the public AppendToOutgoingContext / NewOutgoingContext APIs.
  2. Ensure a single consistent grpc-go version across modules (go mod tidy / check for duplicates with 'go list -m all | grep grpc').
  3. 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

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


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/fa889807a4657825. Report an issue: GitHub.