grpc/grpc-java · error · ResourceInvalidException
A terminal HttpFilter must be the last filter
Error message
A terminal HttpFilter must be the last filter: ${filterName} What it means
gRPC xDS rejects an LDS listener whose HttpConnectionManager's http_filters list places the terminal filter (currently the Router filter) anywhere other than the last position. Envoy semantics require the terminal filter to be the final filter in the chain, so the resource is marked INVALID and the listener is not applied.
Solutions
- Reorder http_filters so the router (terminal) filter is the last entry in the list
- Verify only one terminal filter is present and it terminates the chain
- If the control plane generates the config, fix its filter ordering logic
Example fix
// before
http_filters: [
{name: router, typed_config: {"@type": router}},
{name: cors, typed_config: {"@type": cors}}
]
// after
http_filters: [
{name: cors, typed_config: {"@type": cors}},
{name: router, typed_config: {"@type": router}}
] Defensive patterns
Strategy: validation
Validate before calling
// Java (proto): verify filter order before applying the Listener
boolean lastIsRouter = hcm.getHttpFiltersList().stream()
.filter(f -> f.getTypedConfig().getTypeUrl().contains("router"))
.count() == 1
&& hcm.getHttpFilters(hcm.getHttpFiltersCount() - 1)
.getTypedConfig().getTypeUrl().contains("router");
if (!lastIsRouter) throw new IllegalArgumentException("router filter must be last"); Try / catch
try {
xdsClient.watchResource(LISTENER, name, listenerWatcher);
} catch (ResourceInvalidException e) {
log.error("Invalid listener resource: {}", e.getMessage());
} Prevention
- Always place the router filter last in http_filters
- Keep only one terminal filter per HttpConnectionManager
- Validate Envoy configs with a linter before pushing to the control plane
When it happens
Trigger: Parsing a Listener protobuf (via processClientSideListener -> parseHttpConnectionManager) where http_filters[i] is a terminal filter (isTerminalFilter, i.e. RouterFilter.ROUTER_CONFIG) at index i < httpFiltersCount - 1.
Common situations: Hand-written Envoy v3 listener YAML/JSON with the router filter listed before another filter (e.g. before a Lua or health-check filter); control planes generating filter chains in the wrong order; migrating configs from non-xDS setups where order wasn't enforced.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- AndMatcher must have at least 2 predicates
- client_listener_resource_name_template
- Cluster " + cluster.getName() + ": unspecified cluster…
- common-tls-context is required in upstream-tls-context
- Custom LB config does not contain a JSON object
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/0047cb3b43ab085c.
Report an issue: GitHub.
Appendix: source
Thrown at xds/src/main/java/io/grpc/xds/XdsListenerResource.java:545
"HttpConnectionManager contains duplicate HttpFilter: " + filterName);
}
StructOrError<Filter.FilterConfig> filterConfig =
parseHttpFilter(httpFilter, filterRegistry, isForClient, args);
if ((i == proto.getHttpFiltersCount() - 1)
&& (filterConfig == null || !isTerminalFilter(filterConfig.getStruct()))) {
throw new ResourceInvalidException("The last HttpFilter must be a terminal filter: "
+ filterName);
}
if (filterConfig == null) {
continue;
}
if (filterConfig.getErrorDetail() != null) {
throw new ResourceInvalidException(
"HttpConnectionManager contains invalid HttpFilter: "
+ filterConfig.getErrorDetail());
}
if ((i < proto.getHttpFiltersCount() - 1) && isTerminalFilter(filterConfig.getStruct())) {
throw new ResourceInvalidException("A terminal HttpFilter must be the last filter: "
+ filterName);
}
filterConfigs.add(new Filter.NamedFilterConfig(filterName, filterConfig.getStruct()));
}
// Parse inlined RouteConfiguration or RDS.
if (proto.hasRouteConfig()) {
List<VirtualHost> virtualHosts = extractVirtualHosts(
proto.getRouteConfig(), filterRegistry, args);
return io.grpc.xds.HttpConnectionManager.forVirtualHosts(
maxStreamDuration, virtualHosts, filterConfigs);
}
if (proto.hasRds()) {
Rds rds = proto.getRds();
if (!rds.hasConfigSource()) {
throw new ResourceInvalidException(
"HttpConnectionManager contains invalid RDS: missing config_source");
}View on GitHub (pinned to 64daddc1f3)