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

  1. Reorder http_filters so the router (terminal) filter is the last entry in the list
  2. Verify only one terminal filter is present and it terminates the chain
  3. 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

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


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)