{"record":{"id":"fa889807a4657825","repo":"grpc/grpc-go","slug":"metadata-fromoutgoingcontext-got-an-odd-number-of","errorCode":null,"errorMessage":"metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d","messagePattern":"metadata: FromOutgoingContext got an odd number of input pairs for metadata: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"metadata/metadata.go","lineNumber":352,"sourceCode":"\t\treturn nil, false\n\t}\n\n\tmdSize := len(raw.md)\n\tfor i := range raw.added {\n\t\tmdSize += len(raw.added[i]) / 2\n\t}\n\n\tout := make(MD, mdSize)\n\tfor k, v := range raw.md {\n\t\t// We need to manually convert all keys to lower case, because MD is a\n\t\t// map, and there's no guarantee that the MD attached to the context is\n\t\t// created using our helper functions.\n\t\tkey := strings.ToLower(k)\n\t\tout[key] = copyOf(v)\n\t}\n\tfor _, added := range raw.added {\n\t\tif len(added)%2 == 1 {\n\t\t\tpanic(fmt.Sprintf(\"metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d\", len(added)))\n\t\t}\n\n\t\tfor i := 0; i < len(added); i += 2 {\n\t\t\tkey := strings.ToLower(added[i])\n\t\t\tout[key] = append(out[key], added[i+1])\n\t\t}\n\t}\n\treturn out, ok\n}\n\ntype rawMD struct {\n\tmd    MD\n\tadded [][]string\n}\n","sourceCodeStart":334,"sourceCodeEnd":367,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/metadata/metadata.go#L334-L367","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Always build outgoing metadata with metadata.NewOutgoingContext and metadata.AppendToOutgoingContext; never write the internal rawMD value yourself.","If you use context.WithValue for unrelated data, use your own key types to avoid colliding with grpc's internal keys.","Upgrade grpc-go if you suspect an internal regression; report the bug with a reproducer."],"exampleFix":"// before (forging internal context value -> malformed rawMD.added)\nctx = context.WithValue(ctx, mdOutgoingKey{}, rawMD{added: [][]string{{\"only-a-key\"}}})\nmd, _ := metadata.FromOutgoingContext(ctx) // panic: odd\n\n// after\nctx = metadata.AppendToOutgoingContext(ctx, \"key\", \"value\")\nmd, _ := metadata.FromOutgoingContext(ctx)","handlingStrategy":"validation","validationCode":"// Do not forge the internal rawMD value. Always use the public API:\nctx = metadata.NewOutgoingContext(ctx, md)\nctx = metadata.AppendToOutgoingContext(ctx, \"k\", \"v\")\nmd, ok := metadata.FromOutgoingContext(ctx) // safe","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never write the unexported mdOutgoingKey context value directly.","Use your own typed context keys for non-gRPC data to avoid collisions.","Upgrade grpc-go if you suspect an internal regression and report with a reproducer."],"tags":["metadata","context","panic","internal","validation","go"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}