{"record":{"id":"67030ede0656f50f","repo":"grpc/grpc-java","slug":"filterchainmatch-must-be-unique-found-duplicate","errorCode":null,"errorMessage":"FilterChainMatch must be unique. Found duplicate: ${cur}","messagePattern":"FilterChainMatch must be unique\\. Found duplicate: (.+?)","errorType":"exception","errorClass":"ResourceInvalidException","httpStatus":null,"severity":"error","filePath":"xds/src/main/java/io/grpc/xds/XdsListenerResource.java","lineNumber":328,"sourceCode":"    DownstreamTlsContext.OcspStaplePolicy ocspStaplePolicy = downstreamTlsContext\n        .getOcspStaplePolicy();\n    if (ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.UNRECOGNIZED\n        && ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.LENIENT_STAPLING) {\n      throw new ResourceInvalidException(\n          \"downstream-tls-context with ocsp_staple_policy value \" + ocspStaplePolicy.name()\n              + \" is not supported\");\n    }\n    return downstreamTlsContext;\n  }\n\n  private static void validateFilterChainMatchForUniqueness(\n      Set<FilterChainMatch> filterChainMatchSet,\n      FilterChainMatch filterChainMatch) throws ResourceInvalidException {\n    // Flattens complex FilterChainMatch into a list of simple FilterChainMatch'es.\n    List<FilterChainMatch> crossProduct = getCrossProduct(filterChainMatch);\n    for (FilterChainMatch cur : crossProduct) {\n      if (!filterChainMatchSet.add(cur)) {\n        throw new ResourceInvalidException(\"FilterChainMatch must be unique. \"\n            + \"Found duplicate: \" + cur);\n      }\n    }\n  }\n\n  private static List<FilterChainMatch> getCrossProduct(FilterChainMatch filterChainMatch) {\n    // repeating fields to process:\n    // prefixRanges, applicationProtocols, sourcePrefixRanges, sourcePorts, serverNames\n    List<FilterChainMatch> expandedList = expandOnPrefixRange(filterChainMatch);\n    expandedList = expandOnApplicationProtocols(expandedList);\n    expandedList = expandOnSourcePrefixRange(expandedList);\n    expandedList = expandOnSourcePorts(expandedList);\n    return expandOnServerNames(expandedList);\n  }\n\n  private static List<FilterChainMatch> expandOnPrefixRange(FilterChainMatch filterChainMatch) {\n    ArrayList<FilterChainMatch> expandedList = new ArrayList<>();\n    if (filterChainMatch.prefixRanges().isEmpty()) {","sourceCodeStart":310,"sourceCodeEnd":346,"githubUrl":"https://github.com/grpc/grpc-java/blob/64daddc1f3d1975670f769f3e97bde8b2ba32d25/xds/src/main/java/io/grpc/xds/XdsListenerResource.java#L310-L346","documentation":"Filter chain matches in a Listener must be unambiguous: xDS flattens each FilterChainMatch into a cross-product of simple matches and requires each be unique across the listener. When the same flattened match is seen twice, XdsListenerResource throws ResourceInvalidException because traffic routing would be nondeterministic.","triggerScenarios":"A Listener proto containing two filter_chains whose FilterChainMatch entries overlap exactly after flattening (same destination port, prefix ranges, application protocols, source ranges, server names, transport protocol), detected in validateFilterChainMatchForUniqueness during parseFilterChain.","commonSituations":"Copy-pasted filter chain blocks with identical match criteria but different tls_contexts, control-plane generation bugs emitting duplicate matches, or a partially edited config where wildcards (e.g. empty destination port, empty server name) accidentally broaden one chain to collide with another.","solutions":["Find the two filter_chains with identical FilterChainMatch and make each match unique (narrow ports, destination_prefix_ranges, server_names, or application_protocols).","Remove the redundant duplicate filter chain if it is not needed.","Review wildcard/empty match fields: a chain with an empty match collides with any fully-specified chain sharing its source-type settings."],"exampleFix":"// before: two chains both matching port 443 with no server_names\nfilter_chains: [{ filter_chain_match: { destination_port: 443 } }, { filter_chain_match: { destination_port: 443 } }]\n// after: distinguish by server name or port\nfilter_chains: [{ filter_chain_match: { destination_port: 443, server_names: [\"a.example.com\"] } }, { filter_chain_match: { destination_port: 443, server_names: [\"b.example.com\"] } }]","handlingStrategy":"validation","validationCode":"// dedupe check on filter chain matches before submission\nSet<String> seen = new HashSet<>();\nfor (FilterChain fc : listener.getFilterChainsList()) {\n  if (!seen.add(fc.getFilterChainMatch().toString())) {\n    throw new IllegalArgumentException(\"duplicate FilterChainMatch: \" + fc.getFilterChainMatch());\n  }\n}","typeGuard":null,"tryCatchPattern":"try { parse/apply listener } catch (ResourceInvalidException e) { if (e.getMessage().contains(\"FilterChainMatch must be unique\")) fixDuplicateMatches(); }","preventionTips":["Lint Listener configs for overlapping match criteria in CI","Remember empty match fields act as wildcards and can collide with specific chains","Keep filter chain match fields mutually exclusive across chains"],"tags":["grpc","xds","config-validation","routing"],"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"}