grpc/grpc-go · critical

metadata: AppendToOutgoingContext got an odd number of…

Error message

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

What it means

metadata.AppendToOutgoingContext (metadata/metadata.go:250) appends key/value pairs to the outgoing metadata stored in a context and panics at line 251-253 when len(kv) is odd, for the same reason as Pairs: every key needs a value. The pairs are stored (lowercased keys) and later merged on RPC send.

Solutions

  1. Pass an even number of string arguments (key, value, key, value, ...).
  2. Validate len(kv)%2 == 0 before calling, especially when kv is built dynamically.
  3. Prefer metadata.NewOutgoingContext(ctx, md) with a constructed MD for complex metadata.

Example fix

// before
ctx = metadata.AppendToOutgoingContext(ctx, "auth", token, "request-id") // odd -> panic

// after
ctx = metadata.AppendToOutgoingContext(ctx, "auth", token, "request-id", reqID)
Defensive patterns

Strategy: validation

Validate before calling

func safeAppend(ctx context.Context, kv ...string) context.Context {
    if len(kv)%2 == 1 {
        return ctx // skip malformed append
    }
    return metadata.AppendToOutgoingContext(ctx, kv...)
}

Prevention

When it happens

Trigger: Calling metadata.AppendToOutgoingContext(ctx, "key") with an odd argument count; dynamically building the kv varargs and dropping a value; a helper that conditionally appends a key without its value.

Common situations: Building per-RPC metadata with a typo; a conditional that appends a header name but skips its value in some branch; refactoring call sites and losing an argument.

Related errors


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

Appendix: 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 0c51461d27)