{"record":{"id":"582c1696d40a3671","repo":"grpc/grpc-go","slug":"header-key-is-empty","errorCode":null,"errorMessage":"header key is empty","messagePattern":"header key is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extconfig.go","lineNumber":224,"sourceCode":"\t\t}\n\t\tif !hmr.allow(header) {\n\t\t\tif hmr.DisallowIsError {\n\t\t\t\treturn fmt.Errorf(\"header mutation disallowed by headerMutationRules for header %q\", header)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tinput.Delete(header)\n\t}\n\treturn nil\n}\n\n// validateHeaderKey returns a non-nil error if key may not be mutated by an\n// external processing server, either because the key is reserved or because it\n// is not a valid gRPC header name.\nfunc validateHeaderKey(key string) error {\n\tswitch {\n\tcase len(key) == 0:\n\t\treturn fmt.Errorf(\"header key is empty\")\n\tcase key[0] == ':':\n\t\treturn fmt.Errorf(\"header key %q is a pseudo-header\", key)\n\tcase key == \"host\":\n\t\treturn fmt.Errorf(\"header key %q is reserved\", key)\n\tcase strings.HasPrefix(key, \"grpc-\"):\n\t\treturn fmt.Errorf(\"header key %q is in the reserved 'grpc-' space\", key)\n\tcase key != strings.ToLower(key):\n\t\treturn fmt.Errorf(\"header key %q is not lowercase\", key)\n\tcase len(key) > maxHeaderSize:\n\t\treturn fmt.Errorf(\"header key exceeds the maximum length of %d bytes\", maxHeaderSize)\n\t}\n\treturn imetadata.ValidateKey(key)\n}\n\nfunc (hmr *HeaderMutationRules) allow(key string) bool {\n\tif hmr.DisallowExpr != nil && hmr.DisallowExpr.MatchString(key) {\n\t\treturn false\n\t}","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extconfig.go#L206-L242","documentation":"Returned by validateHeaderKey when the key has length zero. validateHeaderKey is called by both ApplyAdditions (for add/modify mutations) and ApplyRemovals (for removals), so an empty key from the ext_proc server on either path produces this. It is the first case in the validation switch, so it preempts all other checks.","triggerScenarios":"The ext_proc server sends a HeaderValueOption with header.key=\"\" (additions) or includes \"\" in headersToRemove (removals). The client rejects it before applying any mutation.","commonSituations":"Server builds the key from a map lookup that returned zero value; server forwards a header whose name was stripped by an upstream proxy; off-by-one in a header-splitting routine yields an empty token.","solutions":["On the ext_proc server, skip any mutation entry whose key is empty before sending the response.","Validate the key is non-empty at the point of construction (fail fast).","Add logging server-side to surface which code path produced the empty key.","Write a regression test that round-trips mutations with edge-case keys."],"exampleFix":"// before\nfor k, v := range kv { out = append(out, hdrOpt(k, v)) }\n// after\nfor k, v := range kv {\n  if k == \"\" { continue }\n  out = append(out, hdrOpt(k, v))\n}","handlingStrategy":"validation","validationCode":"// server-side: drop empty keys before sending\nfiltered := hvos[:0]\nfor _, h := range hvos {\n  if h.GetHeader().GetKey() == \"\" { continue }\n  filtered = append(filtered, h)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never construct a mutation with an empty key.","Sanitize map-derived keys for zero values.","Add a unit test enumerating edge-case keys.","Log when a key is dropped so bugs surface early."],"tags":["grpc","xds","extproc","header-mutation","validation"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}