grpc/grpc-java · error · ResourceInvalidException

FilterChainMatch must be unique. Found duplicate

Error message

FilterChainMatch must be unique. Found duplicate: ${cur}

What it means

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.

Solutions

  1. Find the two filter_chains with identical FilterChainMatch and make each match unique (narrow ports, destination_prefix_ranges, server_names, or application_protocols).
  2. Remove the redundant duplicate filter chain if it is not needed.
  3. Review wildcard/empty match fields: a chain with an empty match collides with any fully-specified chain sharing its source-type settings.

Example fix

// before: two chains both matching port 443 with no server_names
filter_chains: [{ filter_chain_match: { destination_port: 443 } }, { filter_chain_match: { destination_port: 443 } }]
// after: distinguish by server name or port
filter_chains: [{ filter_chain_match: { destination_port: 443, server_names: ["a.example.com"] } }, { filter_chain_match: { destination_port: 443, server_names: ["b.example.com"] } }]
Defensive patterns

Strategy: validation

Validate before calling

// dedupe check on filter chain matches before submission
Set<String> seen = new HashSet<>();
for (FilterChain fc : listener.getFilterChainsList()) {
  if (!seen.add(fc.getFilterChainMatch().toString())) {
    throw new IllegalArgumentException("duplicate FilterChainMatch: " + fc.getFilterChainMatch());
  }
}

Try / catch

try { parse/apply listener } catch (ResourceInvalidException e) { if (e.getMessage().contains("FilterChainMatch must be unique")) fixDuplicateMatches(); }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/67030ede0656f50f. Report an issue: GitHub.

Appendix: source

Thrown at xds/src/main/java/io/grpc/xds/XdsListenerResource.java:328

    DownstreamTlsContext.OcspStaplePolicy ocspStaplePolicy = downstreamTlsContext
        .getOcspStaplePolicy();
    if (ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.UNRECOGNIZED
        && ocspStaplePolicy != DownstreamTlsContext.OcspStaplePolicy.LENIENT_STAPLING) {
      throw new ResourceInvalidException(
          "downstream-tls-context with ocsp_staple_policy value " + ocspStaplePolicy.name()
              + " is not supported");
    }
    return downstreamTlsContext;
  }

  private static void validateFilterChainMatchForUniqueness(
      Set<FilterChainMatch> filterChainMatchSet,
      FilterChainMatch filterChainMatch) throws ResourceInvalidException {
    // Flattens complex FilterChainMatch into a list of simple FilterChainMatch'es.
    List<FilterChainMatch> crossProduct = getCrossProduct(filterChainMatch);
    for (FilterChainMatch cur : crossProduct) {
      if (!filterChainMatchSet.add(cur)) {
        throw new ResourceInvalidException("FilterChainMatch must be unique. "
            + "Found duplicate: " + cur);
      }
    }
  }

  private static List<FilterChainMatch> getCrossProduct(FilterChainMatch filterChainMatch) {
    // repeating fields to process:
    // prefixRanges, applicationProtocols, sourcePrefixRanges, sourcePorts, serverNames
    List<FilterChainMatch> expandedList = expandOnPrefixRange(filterChainMatch);
    expandedList = expandOnApplicationProtocols(expandedList);
    expandedList = expandOnSourcePrefixRange(expandedList);
    expandedList = expandOnSourcePorts(expandedList);
    return expandOnServerNames(expandedList);
  }

  private static List<FilterChainMatch> expandOnPrefixRange(FilterChainMatch filterChainMatch) {
    ArrayList<FilterChainMatch> expandedList = new ArrayList<>();
    if (filterChainMatch.prefixRanges().isEmpty()) {

View on GitHub (pinned to 64daddc1f3)