{"record":{"id":"3dc06db60c706ab3","repo":"grpc/grpc-go","slug":"extproc-header-mutation-disallowed-by-headermutat","errorCode":null,"errorMessage":"extproc: header mutation disallowed by headerMutationRules for header key %q","messagePattern":"extproc: header mutation disallowed by headerMutationRules for header key %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extconfig.go","lineNumber":143,"sourceCode":"\n\tfor _, hvo := range hvos {\n\t\theader := hvo.GetHeader()\n\t\tkey := header.GetKey()\n\t\tif len(key) == 0 || key[0] == ':' || key == \"host\" || key != strings.ToLower(key) || len(key) > 16384 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvalue := header.GetValue()\n\t\tif strings.HasSuffix(key, \"-bin\") {\n\t\t\tvalue = string(header.GetRawValue())\n\t\t}\n\t\tif len(value) > 16384 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !hmr.allow(key) {\n\t\t\tif hmr.DisallowIsError {\n\t\t\t\treturn fmt.Errorf(\"extproc: header mutation disallowed by headerMutationRules for header key %q\", key)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Perform the mutation on output metadata using the append_action\n\t\t// field from the header value option.\n\t\tswitch hvo.GetAppendAction() {\n\t\tcase v3corepb.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD:\n\t\t\tinput.Append(key, value)\n\t\tcase v3corepb.HeaderValueOption_ADD_IF_ABSENT:\n\t\t\tif input.Get(key) == nil {\n\t\t\t\tinput.Set(key, value)\n\t\t\t}\n\t\tcase v3corepb.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD:\n\t\t\tinput.Set(key, value)\n\t\tcase v3corepb.HeaderValueOption_OVERWRITE_IF_EXISTS:\n\t\t\tif input.Get(key) != nil {\n\t\t\t\tinput.Set(key, value)","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/xds/httpfilter/extconfig.go#L125-L161","documentation":"Raised by (*HeaderMutationRules).ApplyAdditions (extconfig.go:143) when an external server (ext_proc/ext_authz) requests a header addition/modification whose key is rejected by the allow/disallow rules AND DisallowIsError is true. Without DisallowIsError the header is silently skipped (continue at extconfig.go:145); with it set, the whole mutation is treated as an error and the data-plane RPC is failed.","triggerScenarios":"An ext_proc server returns a HeaderMutation.set_headers entry for key K; hmr.allow(K) returns false at extconfig.go:141 and hmr.DisallowIsError is true, so the error is returned. This surfaces back through applyMutations (ext_proc.go:1377) and fails the ext_proc stream (and, in deny mode, the data-plane RPC).","commonSituations":"The mutation_rules allow_expression is too narrow (or disallow_expression too broad) for what the external processor legitimately needs to set; DisallowIsError was enabled for safety but the rules weren't widened to cover the server's actual mutations; the server tries to add a header like 'x-forwarded-for' that falls outside the allowlist.","solutions":["Widen the allow_expression regex (or narrow disallow_expression) so the header key in the %q message is permitted.","Fix the external processor to only mutate headers allowed by the configured rules.","If the rejected mutation is acceptable in your deployment, either turn off DisallowIsError (mutations will be silently dropped) or add the specific header pattern to the allowlist."],"exampleFix":"// before\n//   mutation_rules: {\n//     allow_expression: { regex: \"x-safe-.*\" },\n//     disallow_is_error: { value: true }\n//   }\n//   // ext_proc sets \"x-trace-id\" -> error 369\n//\n// after: allow the additional header\n//   mutation_rules: {\n//     allow_expression: { regex: \"x-(safe|trace)-.*\" },\n//     disallow_is_error: { value: true }\n//   }","handlingStrategy":"validation","validationCode":"// Verify every header the processor will set is allowed BEFORE relying on it,\n// honoring the same allow() logic at extconfig.go:203-214.\nfunc headerAllowed(rules *httpfilter.HeaderMutationRules, key string) bool {\n    return rules.Allow(key) // expose/replicate hmr.allow if unexported\n}\n\n// Ensure your server only emits allowed keys:\nfunc sanitizeSetHeaders(hmr *httpfilter.HeaderMutationRules, hvos []*v3corepb.HeaderValueOption) error {\n    for _, h := range hvos {\n        k := h.GetHeader().GetKey()\n        if k == \"\" || k != strings.ToLower(k) { continue }\n        if !headerAllowed(hmr, k) && hmr.DisallowIsError {\n            return fmt.Errorf(\"set header %q is disallowed\", k)\n        }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"// When applying processor mutations, treat the error as fatal only in deny mode\n// (applyMutations -> failProcStream at ext_proc.go:1377).\nif err := hmr.ApplyAdditions(setHeaders, md); err != nil {\n    // DisallowIsError path: log the offending header and fail/contract the stream.\n    return status.Errorf(codes.Internal, \"header mutation rejected: %v\", err)\n}","preventionTips":["Keep the mutation_rules allowlist in sync with the set of headers your processor actually mutates.","Only enable disallow_is_error once you have confirmed the allowlist covers all server mutations.","In your external processor, reject/validate its own requested mutations against the configured rules before sending."],"tags":["grpc","xds","ext-proc","header-mutation","policy"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}