{"record":{"id":"261afd90e7b35614","repo":"grpc/grpc-go","slug":"header-key-q-is-a-pseudo-header","errorCode":null,"errorMessage":"header key %q is a pseudo-header","messagePattern":"header key %q is a pseudo-header","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extconfig.go","lineNumber":226,"sourceCode":"\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}\n\tif hmr.AllowExpr != nil && hmr.AllowExpr.MatchString(key) {\n\t\treturn true","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extconfig.go#L208-L244","documentation":"Returned by validateHeaderKey when the key begins with ':' (a pseudo-header). HTTP/2 pseudo-headers (:path, :method, :scheme, :authority, :status) are managed by the transport and cannot be mutated by an external processing server. This guards the integrity of the protocol layer.","triggerScenarios":"The ext_proc server returns a mutation to add/remove/modify a key like \":authority\", \":path\", or \":status\". validateHeaderKey hits the key[0]==':' branch at extconfig.go:225.","commonSituations":"Server written for HTTP/1 rewrites passes through ':authority'; a generic header-rewrite rule copies all received headers into a mutation including pseudo-headers; server attempts to rewrite the request path via header mutation.","solutions":["Exclude any key starting with ':' from mutations on the ext_proc server.","To change authority/path, use the proper Envoy/gRPC mechanism (route rewrite, not header mutation).","Filter pseudo-headers out when converting an HTTP/2 header set into mutations.","Add an assertion in server tests that no emitted key starts with ':'."],"exampleFix":"// before\nfor k, v := range allHeaders { emit(k, v) }\n// after\nfor k, v := range allHeaders {\n  if strings.HasPrefix(k, \":\") { continue }\n  emit(k, v)\n}","handlingStrategy":"validation","validationCode":"// server-side: skip pseudo-headers\nif strings.HasPrefix(key, \":\") { return /* skip */ }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat any ':'-prefixed key as off-limits.","Use route rewrite for authority/path changes, not header mutation.","Filter pseudo-headers when converting HTTP/2 headers to mutations.","Document the reserved set for server 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-14T05:17:10.506Z"}