grpc/grpc-go · error

extproc: header mutation disallowed by headerMutationRules f

Error message

extproc: header mutation disallowed by headerMutationRules for header key %q

What it means

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.

Source

Thrown at internal/xds/httpfilter/extconfig.go:143

	for _, hvo := range hvos {
		header := hvo.GetHeader()
		key := header.GetKey()
		if len(key) == 0 || key[0] == ':' || key == "host" || key != strings.ToLower(key) || len(key) > 16384 {
			continue
		}

		value := header.GetValue()
		if strings.HasSuffix(key, "-bin") {
			value = string(header.GetRawValue())
		}
		if len(value) > 16384 {
			continue
		}

		if !hmr.allow(key) {
			if hmr.DisallowIsError {
				return fmt.Errorf("extproc: header mutation disallowed by headerMutationRules for header key %q", key)
			}
			continue
		}

		// Perform the mutation on output metadata using the append_action
		// field from the header value option.
		switch hvo.GetAppendAction() {
		case v3corepb.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD:
			input.Append(key, value)
		case v3corepb.HeaderValueOption_ADD_IF_ABSENT:
			if input.Get(key) == nil {
				input.Set(key, value)
			}
		case v3corepb.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD:
			input.Set(key, value)
		case v3corepb.HeaderValueOption_OVERWRITE_IF_EXISTS:
			if input.Get(key) != nil {
				input.Set(key, value)

View on GitHub (pinned to 03255a9237)

Solutions

  1. Widen the allow_expression regex (or narrow disallow_expression) so the header key in the %q message is permitted.
  2. Fix the external processor to only mutate headers allowed by the configured rules.
  3. 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.

Example fix

// before
//   mutation_rules: {
//     allow_expression: { regex: "x-safe-.*" },
//     disallow_is_error: { value: true }
//   }
//   // ext_proc sets "x-trace-id" -> error 369
//
// after: allow the additional header
//   mutation_rules: {
//     allow_expression: { regex: "x-(safe|trace)-.*" },
//     disallow_is_error: { value: true }
//   }
Defensive patterns

Strategy: validation

Validate before calling

// Verify every header the processor will set is allowed BEFORE relying on it,
// honoring the same allow() logic at extconfig.go:203-214.
func headerAllowed(rules *httpfilter.HeaderMutationRules, key string) bool {
    return rules.Allow(key) // expose/replicate hmr.allow if unexported
}

// Ensure your server only emits allowed keys:
func sanitizeSetHeaders(hmr *httpfilter.HeaderMutationRules, hvos []*v3corepb.HeaderValueOption) error {
    for _, h := range hvos {
        k := h.GetHeader().GetKey()
        if k == "" || k != strings.ToLower(k) { continue }
        if !headerAllowed(hmr, k) && hmr.DisallowIsError {
            return fmt.Errorf("set header %q is disallowed", k)
        }
    }
    return nil
}

Try / catch

// When applying processor mutations, treat the error as fatal only in deny mode
// (applyMutations -> failProcStream at ext_proc.go:1377).
if err := hmr.ApplyAdditions(setHeaders, md); err != nil {
    // DisallowIsError path: log the offending header and fail/contract the stream.
    return status.Errorf(codes.Internal, "header mutation rejected: %v", err)
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/3dc06db60c706ab3. Report an issue: GitHub.