grpc/grpc-java · error · HeaderMutationDisallowedException
Header mutation disallowed
Error message
Header mutation disallowed
What it means
The HeaderMutationFilter enforces xDS header mutation rules (per gRFC A48/Envoy behavior). When a header add/modify/remove entry is neither explicitly allowed by the rules nor marked as ignorable, and disallowIsError() is true (the default), it throws HeaderMutationDisallowedException. This matches Envoy's default of rejecting disallowed header mutations rather than silently dropping them.
Source
Thrown at xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationFilter.java:75
/**
* A generic helper to filter a collection based on a predicate.
*/
private <T> ImmutableList<T> filterCollection(Collection<T> items,
Predicate<T> isIgnoredPredicate, Predicate<T> isAllowedPredicate)
throws HeaderMutationDisallowedException {
ImmutableList.Builder<T> allowed = ImmutableList.builder();
for (T item : items) {
boolean isIgnored = isIgnoredPredicate.test(item);
boolean isAllowed = isAllowedPredicate.test(item);
// TODO(sauravzg): The specification is ambiguous regarding whether system headers
// should be silently ignored or trigger an error when disallowIsError is enabled.
// We default to triggering errors matching Envoy's implementation.
// Ref: https://github.com/grpc/proposal/pull/481#discussion_r3124453674
if (!isIgnored && isAllowed) {
allowed.add(item);
} else if (disallowIsError()) {
throw new HeaderMutationDisallowedException("Header mutation disallowed");
}
}
return allowed.build();
}
private boolean isDisallowed(String key) {
return HeaderValueValidationUtils.isDisallowed(key);
}
private boolean isDisallowed(HeaderValueOption option) {
return HeaderValueValidationUtils.isDisallowed(option.header());
}
private boolean isHeaderMutationAllowed(HeaderValueOption option) {
return isHeaderMutationAllowed(option.header().key());
}
private boolean isHeaderMutationAllowed(String headerName) {View on GitHub (pinned to 64daddc1f3)
Solutions
- Remove the disallowed header mutation from the xDS configuration (pseudo-headers and protected keys cannot be mutated)
- Bring the header key within the rules' allowed_regular_expressions if it should be permitted
- Set disallow_is_error: false in the HeaderMutation rules so disallowed mutations are silently ignored instead of failing
- Validate the header keys against the rules before deploying the config
Example fix
// before (xDS config JSON)
{"mutation": {"add": {"header": ":path", "value": "/foo"}}}
// after: pseudo-headers cannot be mutated; mutate a custom header instead
{"mutation": {"add": {"header": "x-forwarded-path", "value": "/foo"}}} Defensive patterns
Strategy: try-catch
Validate before calling
HeaderMutationFilter filter = HeaderMutationFilter.create(rules);
// pre-check keys before submitting mutations
for (String key : mutationKeys) {
if (key.startsWith(":") || List.of("host","content-length").contains(key)) {
throw new ConfigException("Cannot mutate protected header: " + key);
}
} Try / catch
try {
List<HeaderMutation> allowed = filter.allowedHeaders(mutations);
} catch (HeaderMutationDisallowedException e) {
log.error("xDS header mutation not permitted by rules: {}", e.getMessage());
} Prevention
- Never attempt to mutate pseudo-headers (:path, :authority)
- Match custom headers against allowed_regular_expressions before deploying
- Set disallow_is_error: false only if silent dropping is acceptable
- Validate configs against Envoy's documented header mutation restrictions
When it happens
Trigger: filterCollection() (invoked via allowedHeaders/allowedHeadersToRemove) encounters a header mutation whose key is disallowed (e.g., pseudo-headers like ":path", or keys not matching allowed_regular_expressions) while disallow_is_error is enabled.
Common situations: xDS route config or HTTP filter config attempts to add/set a protected header (:authority, :path, host, etc.) or a header not matching allow rules; configs written for a proxy that silently ignores mutations being run against this stricter implementation.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- unsupported ExtAuthz service type: only grpc_service is supp
- Invalid ring hash function: " + ringHash.getHashFunction()
- Custom LB config does not contain a JSON object
- Invalid header matcher config: [grpc-] prefixed header name
- Invalid header matcher config: header name [:scheme] is not
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/d69d21b7413c2cae.
Report an issue: GitHub.