From 777e14da64154eed2386750c1e74a132be4618f3 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 24 Mar 2026 15:11:31 -0700 Subject: [PATCH 1/3] xds: Handle empty target URI authorities the same as null. Fixes an oversight in #12660. io.grpc.Uri#getAuthority makes a distinction between xds:///service and xds:/service but java.net.URI does not. XdsNameResolver wants to treat those two cases the same. --- .../java/io/grpc/xds/XdsNameResolver.java | 11 +++++- .../io/grpc/xds/XdsNameResolverProvider.java | 6 +++- .../java/io/grpc/xds/XdsNameResolverTest.java | 34 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index d4b06a24eb5..03a29025ff6 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -138,6 +138,13 @@ final class XdsNameResolver extends NameResolver { private CallCounterProvider callCounterProvider; private ResolveState resolveState; + /** + * Constructs a new instance. + * + * @param target the target URI to resolve + * @param targetAuthority the authority component of `target`, possibly the empty string, or null + * if 'target' has no such component + */ XdsNameResolver( String target, @Nullable String targetAuthority, String name, @Nullable String overrideAuthority, ServiceConfigParser serviceConfigParser, @@ -208,7 +215,9 @@ public void start(Listener2 listener) { } BootstrapInfo bootstrapInfo = xdsClient.getBootstrapInfo(); String listenerNameTemplate; - if (targetAuthority == null) { + if (targetAuthority == null || targetAuthority.isEmpty()) { + // Both https://github.com/grpc/proposal/blob/master/A27-xds-global-load-balancing.md and + // A47-xds-federation.md seem to treat an empty authority the same as an undefined one. listenerNameTemplate = bootstrapInfo.clientDefaultListenerResourceNameTemplate(); } else { AuthorityInfo authorityInfo = bootstrapInfo.authorities().get(targetAuthority); diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java index 89e51694a69..51b1ff49bf0 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java @@ -87,7 +87,11 @@ public XdsNameResolver newNameResolver(URI targetUri, Args args) { targetPath, targetUri); String name = targetPath.substring(1); - return newNameResolver(targetUri.toString(), targetUri.getAuthority(), name, args); + // TODO(jdcormie): java.net.URI#getAuthority incorrectly returns null for both xds:///service + // and xds:/service. This doesn't matter for now since XdsNameResolver treats them the same + // anyway and all this code will go away once newNameResolver(io.grpc.Uri) launches. + String targetAuthority = targetUri.getAuthority(); + return newNameResolver(targetUri.toString(), targetAuthority, name, args); } return null; } diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index d8e63704a45..e78f97635ed 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -297,6 +297,40 @@ public void resolving_noTargetAuthority_templateWithoutXdstp() { verify(mockListener, never()).onError(any(Status.class)); } + @Test + public void resolving_emptyTargetAuthority_templateWithXdstp() { + bootstrapInfo = + BootstrapInfo.builder() + .servers( + ImmutableList.of( + ServerInfo.create("td.googleapis.com", InsecureChannelCredentials.create()))) + .node(Node.newBuilder().build()) + .clientDefaultListenerResourceNameTemplate( + "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/%s?id=1") + .build(); + String serviceAuthority = "[::FFFF:129.144.52.38]:80"; + expectedLdsResourceName = + "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + + "%5B::FFFF:129.144.52.38%5D:80?id=1"; + resolver = + new XdsNameResolver( + "xds:///foo.googleapis.com", + "", + serviceAuthority, + null, + serviceConfigParser, + syncContext, + scheduler, + xdsClientPoolFactory, + mockRandom, + FilterRegistry.getDefaultRegistry(), + rawBootstrap, + metricRecorder, + nameResolverArgs); + resolver.start(mockListener); + verify(mockListener, never()).onError(any(Status.class)); + } + @Test public void resolving_noTargetAuthority_templateWithXdstp() { bootstrapInfo = BootstrapInfo.builder() From 1873374fb3ca7fdbfdc4fac5d140bb62293d0c54 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 24 Mar 2026 16:54:36 -0700 Subject: [PATCH 2/3] api: create a test_fixtures bazel target --- MODULE.bazel | 1 + api/BUILD.bazel | 18 ++++++++++++++++++ repositories.bzl | 1 + 3 files changed, 20 insertions(+) diff --git a/MODULE.bazel b/MODULE.bazel index 85c8b1aec13..8e911022e85 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -41,6 +41,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", + "org.mockito:mockito-core:4.4.0", "org.checkerframework:checker-qual:3.49.5", "org.codehaus.mojo:animal-sniffer-annotations:1.26", ] diff --git a/api/BUILD.bazel b/api/BUILD.bazel index 4c5e750b5e4..6de00d6272d 100644 --- a/api/BUILD.bazel +++ b/api/BUILD.bazel @@ -15,3 +15,21 @@ java_library( artifact("com.google.guava:guava"), ], ) + +java_library( + name = "test_fixtures", + testonly = 1, + srcs = glob([ + "src/testFixtures/java/io/grpc/**/*.java", + ]), + visibility = ["//xds:__pkg__"], + deps = [ + "//core", + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), + artifact("com.google.guava:guava"), + artifact("com.google.truth:truth"), + artifact("junit:junit"), + artifact("org.mockito:mockito-core"), + ], +) diff --git a/repositories.bzl b/repositories.bzl index de0729c35ab..bc502e430c0 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -45,6 +45,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", + "org.mockito:mockito-core:4.4.0", "org.checkerframework:checker-qual:3.49.5", "org.codehaus.mojo:animal-sniffer-annotations:1.26", ] From c6cb102fe6cef5a67ac069876c6902e65f386921 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 24 Mar 2026 16:55:18 -0700 Subject: [PATCH 3/3] xds: Run integration test with both values of RFC 3986 URI flag --- xds/BUILD.bazel | 1 + .../FakeControlPlaneXdsIntegrationTest.java | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 7f2c5f02338..9a650485c6c 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -343,6 +343,7 @@ java_library( ":xds", ":xds_java_proto", "//api", + "//api:test_fixtures", "//core:internal", "//stub", "//testing-proto:simpleservice_java_grpc", diff --git a/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java b/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java index c8f2b8932ef..a273c6f3ebf 100644 --- a/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java +++ b/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java @@ -50,8 +50,10 @@ import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.ClientStreamTracer; +import io.grpc.FlagResetRule; import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; import io.grpc.ForwardingClientCallListener; +import io.grpc.InternalFeatureFlags; import io.grpc.LoadBalancerRegistry; import io.grpc.ManagedChannel; import io.grpc.Metadata; @@ -60,10 +62,14 @@ import io.grpc.testing.protobuf.SimpleResponse; import io.grpc.testing.protobuf.SimpleServiceGrpc; import java.net.InetSocketAddress; +import java.util.Arrays; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; /** * Xds integration tests using a local control plane, implemented in {@link @@ -85,13 +91,28 @@ * 3) Construct EDS config w/ test server address from 2). Set CDS and EDS Config at the Control * Plane. Then start the test xDS client (requires EDS to do xDS name resolution). */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class FakeControlPlaneXdsIntegrationTest { @Rule(order = 0) public ControlPlaneRule controlPlane = new ControlPlaneRule(); @Rule(order = 1) public DataPlaneRule dataPlane = new DataPlaneRule(controlPlane); + @Rule(order = 2) + public final FlagResetRule flagResetRule = new FlagResetRule(); + + @Parameters(name = "enableRfc3986UrisParam={0}") + public static Iterable data() { + return Arrays.asList(new Object[][] {{true}, {false}}); + } + + @Parameter public boolean enableRfc3986UrisParam; + + @Before + public void setupRfc3986UrisFeatureFlag() throws Exception { + flagResetRule.setFlagForTest( + InternalFeatureFlags::setRfc3986UrisEnabled, enableRfc3986UrisParam); + } @Test public void pingPong() throws Exception {