{"record":{"id":"b4e0d8b03ac979a3","repo":"grpc/grpc-java","slug":"the-last-httpfilter-must-be-a-terminal-filter-f","errorCode":null,"errorMessage":"The last HttpFilter must be a terminal filter: ${filterName}","messagePattern":"The last HttpFilter must be a terminal filter: (.+?)","errorType":"exception","errorClass":"ResourceInvalidException","httpStatus":null,"severity":"error","filePath":"xds/src/main/java/io/grpc/xds/XdsListenerResource.java","lineNumber":533,"sourceCode":"    // Parse http filters.\n    if (proto.getHttpFiltersList().isEmpty()) {\n      throw new ResourceInvalidException(\"Missing HttpFilter in HttpConnectionManager.\");\n    }\n    List<Filter.NamedFilterConfig> filterConfigs = new ArrayList<>();\n    Set<String> names = new HashSet<>();\n    for (int i = 0; i < proto.getHttpFiltersCount(); i++) {\n      io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter\n          httpFilter = proto.getHttpFiltersList().get(i);\n      String filterName = httpFilter.getName();\n      if (!names.add(filterName)) {\n        throw new ResourceInvalidException(\n            \"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.","sourceCodeStart":515,"sourceCodeEnd":551,"githubUrl":"https://github.com/grpc/grpc-java/blob/64daddc1f3d1975670f769f3e97bde8b2ba32d25/xds/src/main/java/io/grpc/xds/XdsListenerResource.java#L515-L551","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nhttp_filters: [{ name: cors, ... }, { name: grpc_logger, ... }]\n// after\nhttp_filters: [{ name: cors, ... }, { name: envoy.filters.http.router, ... }]","handlingStrategy":"validation","validationCode":"HttpFilter last = hcm.getHttpFilters(hcm.getHttpFiltersCount() - 1);\nif (!last.getName().equals(\"envoy.filters.http.router\")) {\n  throw new IllegalArgumentException(\"last HttpFilter must be the router/terminal filter\");\n}","typeGuard":null,"tryCatchPattern":"try { applyResource(listener) } catch (ResourceInvalidException e) { if (e.getMessage().startsWith(\"The last HttpFilter must be a terminal filter\")) moveRouterFilterLast(); }","preventionTips":["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"],"tags":["grpc","xds","http","config-validation"],"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-14T05:17:10.506Z"}