grpc/grpc-java · error · ResourceInvalidException
The last HttpFilter must be a terminal filter: ${filterName}
Error message
The last HttpFilter must be a terminal filter: ${filterName} What it means
The last HTTP filter in an HttpConnectionManager must be a terminal filter (one that terminates request processing, such as the router filter). If the final filter is missing, not parseable, or non-terminal, XdsListenerResource throws ResourceInvalidException naming the offending filter.
Source
Thrown at xds/src/main/java/io/grpc/xds/XdsListenerResource.java:533
// Parse http filters.
if (proto.getHttpFiltersList().isEmpty()) {
throw new ResourceInvalidException("Missing HttpFilter in HttpConnectionManager.");
}
List<Filter.NamedFilterConfig> filterConfigs = new ArrayList<>();
Set<String> names = new HashSet<>();
for (int i = 0; i < proto.getHttpFiltersCount(); i++) {
io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter
httpFilter = proto.getHttpFiltersList().get(i);
String filterName = httpFilter.getName();
if (!names.add(filterName)) {
throw new ResourceInvalidException(
"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.View on GitHub (pinned to 64daddc1f3)
Solutions
- Ensure envoy.filters.http.router (gRPC's router filter) is the last entry in http_filters.
- Move any non-terminal filters (auth, cors, etc.) before the terminal router filter.
- If the last filter parses to null, its name/type is unknown to this gRPC version — use a recognized filter name or upgrade the library.
Example fix
// before
http_filters: [{ name: cors, ... }, { name: grpc_logger, ... }]
// after
http_filters: [{ name: cors, ... }, { name: envoy.filters.http.router, ... }] Defensive patterns
Strategy: validation
Validate before calling
HttpFilter last = hcm.getHttpFilters(hcm.getHttpFiltersCount() - 1);
if (!last.getName().equals("envoy.filters.http.router")) {
throw new IllegalArgumentException("last HttpFilter must be the router/terminal filter");
} Try / catch
try { applyResource(listener) } catch (ResourceInvalidException e) { if (e.getMessage().startsWith("The last HttpFilter must be a terminal filter")) moveRouterFilterLast(); } Prevention
- Always append the router filter last after any custom filters
- Never append post-processing filters after the terminal filter
- Only reference filters known to the gRPC filter registry so the terminal check parses them
When it happens
Trigger: http_filters where the last entry's parsed config is null (unknown/unregistered filter) or isTerminalFilter(...) returns false, checked at i == getHttpFiltersCount()-1 in parseHttpConnectionManager.
Common situations: Configs where a logging/metadata filter was appended after the router filter, the router filter omitted entirely leaving a non-terminal filter last, or a custom filter name the gRPC filter registry doesn't know (parse yields null).
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
- Missing HttpFilter in HttpConnectionManager.
- HttpConnectionManager contains duplicate HttpFilter: ${filte
- unsupported ExtAuthz service type: only grpc_service is supp
- Invalid ring hash function: " + ringHash.getHashFunction()
- 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/b4e0d8b03ac979a3.
Report an issue: GitHub.