{"record":{"id":"33f917aed82118f3","repo":"grpc/grpc-go","slug":"header-mutation-disallowed-by-headermutationrules-33f917","errorCode":null,"errorMessage":"header mutation disallowed by headerMutationRules for header %q","messagePattern":"header mutation disallowed by headerMutationRules for header %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extconfig.go","lineNumber":209,"sourceCode":"// The input metadata must not be nil.\nfunc (hmr *HeaderMutationRules) ApplyRemovals(headersToRemove []string, input metadata.MD) error {\n\tif hmr == nil {\n\t\thmr = &HeaderMutationRules{}\n\t}\n\tif input == nil {\n\t\treturn fmt.Errorf(\"input metadata is nil\")\n\t}\n\tif hmr.DisallowAll {\n\t\treturn nil\n\t}\n\n\tfor _, header := range headersToRemove {\n\t\tif err := validateHeaderKey(header); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid header mutation: %v\", err)\n\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\":","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extconfig.go#L191-L227","documentation":"Returned by ApplyRemovals when a header the ext_proc server wants to remove is blocked by HeaderMutationRules and DisallowIsError is true. Same policy mechanism as error 442, but on the removal path (extconfig.go:209). When DisallowIsError is false the removal is silently skipped.","triggerScenarios":"xDS config has mutation_rules.disallow_is_error=true and the ext_proc server returns a request to delete a header that fails allow() (matches disallow_expression, or allow_expression is set and does not match).","commonSituations":"Policy forbids deleting x-trace-* but the server tries to strip it; a generic 'remove all headers' implementation in the server runs afoul of a strict allow list; regex change deployed without updating the server.","solutions":["Stop requesting removal of headers the policy protects; only remove explicitly allowed names.","Adjust allow_expression / disallow_expression to permit the intended removal.","Set disallow_is_error=false if silent skip is acceptable for your threat model.","Cross-check the regex semantics against Go's RE2 (no backreferences, anchored differently)."],"exampleFix":"// before: server blanket-removes everything it did not set\nfor k := range incoming { resp.Remove(k) }\n// after: respect an allowlist\nfor k := range incoming {\n  if allowedRemoval(k) { resp.Remove(k) }\n}","handlingStrategy":"validation","validationCode":"// server-side: only request removal of policy-allowed headers\nfor _, k := range toRemove {\n  if !policy.Allowed(k) { continue }\n  resp.Remove(k)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Share the allow/disallow regex config with the server.","Consider disallow_is_error=false for resilience.","Avoid blanket 'remove all' loops on the server.","Re-test policy changes against the server's emitted set."],"tags":["grpc","xds","extproc","header-mutation","policy"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}