grpc/grpc-go · critical

metadata: AppendToOutgoingContext got an odd number of input

Error message

metadata: AppendToOutgoingContext got an odd number of input pairs for metadata: %d

What it means

metadata.AppendToOutgoingContext (metadata.go:250) appends key/value pairs to the outgoing metadata already attached to a context. Like Pairs, it panics on an odd argument count because each key must be followed by exactly one value. This is called per-RPC (inside request building), so the panic occurs in the hot path and will crash the serving goroutine.

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 03255a9237)

Solutions

  1. Locate the AppendToOutgoingContext call in the panic stack trace and ensure arguments are even and paired.
  2. For conditional headers, always append both the key and the value together inside the same branch.
  3. Add a unit test that asserts len(args)%2==0 for any dynamic pair slice you build.

Example fix

// before
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token, "caller") // odd

// after
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token, "caller", callerID)
Defensive patterns

Strategy: validation

Validate before calling

// Guard dynamic per-RPC metadata before appending
if len(pairs)%2 != 0 {
    return ctx, fmt.Errorf("AppendToOutgoingContext needs even pairs, got %d", len(pairs))
}
return metadata.AppendToOutgoingContext(ctx, pairs...), nil

Prevention

When it happens

Trigger: Calling metadata.AppendToOutgoingContext(ctx, kv...) with an odd number of arguments, e.g. AppendToOutgoingContext(ctx, "auth", token, "extra"). The len(kv)%2==1 guard fires immediately.

Common situations: Per-RPC interceptor or handler appending a header but forgetting the value; a conditional append that added the key branch but not the value; refactoring that dropped an argument.

Related errors


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