{"record":{"id":"0047cb3b43ab085c","repo":"grpc/grpc-java","slug":"a-terminal-httpfilter-must-be-the-last-filter-f","errorCode":null,"errorMessage":"A terminal HttpFilter must be the last filter: ${filterName}","messagePattern":"A terminal HttpFilter must be the last filter: (.+?)","errorType":"exception","errorClass":"ResourceInvalidException","httpStatus":null,"severity":"error","filePath":"xds/src/main/java/io/grpc/xds/XdsListenerResource.java","lineNumber":545,"sourceCode":"            \"HttpConnectionManager contains duplicate HttpFilter: \" + filterName);\n      }\n      StructOrError<Filter.FilterConfig> filterConfig =\n          parseHttpFilter(httpFilter, filterRegistry, isForClient, args);\n      if ((i == proto.getHttpFiltersCount() - 1)\n          && (filterConfig == null || !isTerminalFilter(filterConfig.getStruct()))) {\n        throw new ResourceInvalidException(\"The last HttpFilter must be a terminal filter: \"\n            + filterName);\n      }\n      if (filterConfig == null) {\n        continue;\n      }\n      if (filterConfig.getErrorDetail() != null) {\n        throw new ResourceInvalidException(\n            \"HttpConnectionManager contains invalid HttpFilter: \"\n                + filterConfig.getErrorDetail());\n      }\n      if ((i < proto.getHttpFiltersCount() - 1) && isTerminalFilter(filterConfig.getStruct())) {\n        throw new ResourceInvalidException(\"A terminal HttpFilter must be the last filter: \"\n            + filterName);\n      }\n      filterConfigs.add(new Filter.NamedFilterConfig(filterName, filterConfig.getStruct()));\n    }\n\n    // Parse inlined RouteConfiguration or RDS.\n    if (proto.hasRouteConfig()) {\n      List<VirtualHost> virtualHosts = extractVirtualHosts(\n          proto.getRouteConfig(), filterRegistry, args);\n      return io.grpc.xds.HttpConnectionManager.forVirtualHosts(\n          maxStreamDuration, virtualHosts, filterConfigs);\n    }\n    if (proto.hasRds()) {\n      Rds rds = proto.getRds();\n      if (!rds.hasConfigSource()) {\n        throw new ResourceInvalidException(\n            \"HttpConnectionManager contains invalid RDS: missing config_source\");\n      }","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/grpc/grpc-java/blob/64daddc1f3d1975670f769f3e97bde8b2ba32d25/xds/src/main/java/io/grpc/xds/XdsListenerResource.java#L527-L563","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nhttp_filters: [\n  {name: router, typed_config: {\"@type\": router}},\n  {name: cors, typed_config: {\"@type\": cors}}\n]\n// after\nhttp_filters: [\n  {name: cors, typed_config: {\"@type\": cors}},\n  {name: router, typed_config: {\"@type\": router}}\n]","handlingStrategy":"validation","validationCode":"// Java (proto): verify filter order before applying the Listener\nboolean lastIsRouter = hcm.getHttpFiltersList().stream()\n    .filter(f -> f.getTypedConfig().getTypeUrl().contains(\"router\"))\n    .count() == 1\n    && hcm.getHttpFilters(hcm.getHttpFiltersCount() - 1)\n        .getTypedConfig().getTypeUrl().contains(\"router\");\nif (!lastIsRouter) throw new IllegalArgumentException(\"router filter must be last\");","typeGuard":null,"tryCatchPattern":"try {\n  xdsClient.watchResource(LISTENER, name, listenerWatcher);\n} catch (ResourceInvalidException e) {\n  log.error(\"Invalid listener resource: {}\", e.getMessage());\n}","preventionTips":["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"],"tags":["xds","grpc","config-validation","http-filter"],"backgroundTag":"invalid-config-value","analyzedSha":"64daddc1f3d1975670f769f3e97bde8b2ba32d25","analyzedAt":"2026-09-08T06:14:57.704Z","contentChangedAt":"2026-09-08T06:14:57.704Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}