{"record":{"id":"52c4cd9bf046de75","repo":"grpc/grpc-go","slug":"header-key-q-is-reserved","errorCode":null,"errorMessage":"header key %q is reserved","messagePattern":"header key %q is reserved","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extconfig.go","lineNumber":228,"sourceCode":"\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}\n\tif hmr.AllowExpr != nil && hmr.AllowExpr.MatchString(key) {\n\t\treturn true\n\t}\n\tif hmr.AllowExpr != nil {","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extconfig.go#L210-L246","documentation":"Returned by validateHeaderKey when the key is exactly \"host\". The host/authority is reserved (it maps to the :authority pseudo-header in HTTP/2), so the transport owns it and an external processor may not mutate it directly. This is the third case in the validation switch.","triggerScenarios":"The ext_proc server returns an add/remove/modify mutation for the literal header name \"host\" (lowercase). validateHeaderKey matches key == \"host\" at extconfig.go:227.","commonSituations":"Server ported from HTTP/1 attempts to rewrite Host; a generic 'copy all headers' loop includes host; a security filter tries to strip host to hide the upstream.","solutions":["Do not mutate the \"host\" header from an ext_proc server.","To influence authority, configure route host-rewrite in the xDS RouteConfiguration instead.","Skip \"host\" explicitly when building mutations from an inbound header set.","Document the reserved set (host, :*, grpc-*) in the server's authoring guide."],"exampleFix":"// before\nemit(\"host\", newAuthority)\n// after: use route rewrite, not header mutation\n// (configure virtual host rewrite: request_headers_to_add is NOT for host)","handlingStrategy":"validation","validationCode":"// server-side: never mutate host\nif key == \"host\" { return /* skip */ }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Reserve 'host' as immutable on the server.","Use xDS route host-rewrite to change authority.","Skip 'host' in copy-all loops.","Document host/:authority mapping for authors."],"tags":["grpc","xds","extproc","header-mutation","http2"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}