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

  1. Ensure envoy.filters.http.router (gRPC's router filter) is the last entry in http_filters.
  2. Move any non-terminal filters (auth, cors, etc.) before the terminal router filter.
  3. 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

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


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/b4e0d8b03ac979a3. Report an issue: GitHub.