From b5403b4406eba955baafb09e6bc869ab341a510e Mon Sep 17 00:00:00 2001 From: siddiqui irshad Date: Thu, 29 Oct 2026 08:05:41 +0530 Subject: [PATCH] fix(core): support hostnames whose last label starts with a digit In DnsNameResolver and GrpcUtil.authorityFromHostAndPort, hostnames were validated and parsed via java.net.URI. Because java.net.URI implements RFC 2396 grammar where the toplabel of a hostname must begin with an alphabetic character (JDK-8188305), valid RFC 1123 hostnames whose last label starts with a digit (e.g. Kubernetes services in namespaces like otlp.1234-k8s-namespace) resulted in URI.getHost() returning null and URI constructors throwing URISyntaxException. This change: 1. In DnsNameResolver, if nameUri.getHost() is null due to RFC 2396 restrictions, extracts host and port from nameUri.getAuthority(). 2. In GrpcUtil.authorityFromHostAndPort, falls back to manual host/port concatenation validated through checkAuthority() when new URI(...) throws URISyntaxException. 3. Adds unit tests in DnsNameResolverProviderTest and GrpcUtilTest. Fixes #13075 Signed-off-by: siddiqui irshad --- .../io/grpc/internal/DnsNameResolver.java | 28 +++++++++++++++---- .../main/java/io/grpc/internal/GrpcUtil.java | 10 ++++++- .../internal/DnsNameResolverProviderTest.java | 10 +++++++ .../java/io/grpc/internal/GrpcUtilTest.java | 6 ++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolver.java b/core/src/main/java/io/grpc/internal/DnsNameResolver.java index 1c1d95ed616..7afb53d20ed 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolver.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolver.java @@ -163,14 +163,32 @@ protected DnsNameResolver( // Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an // opaque URI, thus the authority and host of the resulted URI would be null. URI nameUri = URI.create("//" + checkNotNull(name, "name")); - Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name); authority = Preconditions.checkNotNull(nameUri.getAuthority(), "nameUri (%s) doesn't have an authority", nameUri); - host = nameUri.getHost(); - if (nameUri.getPort() == -1) { - port = args.getDefaultPort(); + if (nameUri.getHost() != null) { + host = nameUri.getHost(); + if (nameUri.getPort() == -1) { + port = args.getDefaultPort(); + } else { + port = nameUri.getPort(); + } } else { - port = nameUri.getPort(); + // JDK-8188305: java.net.URI.getHost() returns null when the last label of a hostname + // starts with a digit (e.g. otlp.1234-k8s-namespace). Parse host and port from authority. + Preconditions.checkArgument(authority.indexOf('@') == -1, "Invalid DNS name: %s", name); + int colonIndex = authority.lastIndexOf(':'); + if (colonIndex != -1 && (authority.indexOf(']') == -1 || colonIndex > authority.lastIndexOf(']'))) { + host = authority.substring(0, colonIndex); + try { + port = Integer.parseInt(authority.substring(colonIndex + 1)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid DNS name: " + name, e); + } + } else { + host = authority; + port = args.getDefaultPort(); + } + Preconditions.checkArgument(!host.isEmpty(), "Invalid DNS name: %s", name); } this.proxyDetector = checkNotNull(args.getProxyDetector(), "proxyDetector"); Executor offloadExecutor = args.getOffloadExecutor(); diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 0e962ba0e33..6759d388c9c 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -547,7 +547,15 @@ public static String authorityFromHostAndPort(String host, int port) { try { return new URI(null, null, host, port, null, null, null).getAuthority(); } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Invalid host or port: " + host + " " + port, ex); + // JDK-8188305: java.net.URI rejects valid RFC 1123 hostnames whose last label starts with + // a digit (e.g. otlp.1234-k8s-namespace). Fall back to manual combination and checkAuthority. + String formattedHost = (host.contains(":") && !host.startsWith("[")) ? "[" + host + "]" : host; + String authority = formattedHost + ":" + port; + try { + return checkAuthority(authority); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid host or port: " + host + " " + port, ex); + } } } diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java index 75b82df544f..7cf11e5eba5 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java @@ -79,6 +79,16 @@ public void newNameResolver_acceptsHostAndPort() { assertThat(((DnsNameResolver) nameResolver).getPort()).isEqualTo(443); } + @Test + public void newNameResolver_acceptsHostStartingWithDigit() { + NameResolver nameResolver = newNameResolver("dns:///otlp.1234-k8s-namespace:4317", args); + assertThat(nameResolver).isNotNull(); + assertThat(nameResolver.getClass()).isSameInstanceAs(DnsNameResolver.class); + assertThat(nameResolver.getServiceAuthority()).isEqualTo("otlp.1234-k8s-namespace:4317"); + assertThat(((DnsNameResolver) nameResolver).getHost()).isEqualTo("otlp.1234-k8s-namespace"); + assertThat(((DnsNameResolver) nameResolver).getPort()).isEqualTo(4317); + } + @Test public void newNameResolver_acceptsRootless() { assume().that(enableRfc3986UrisParam).isTrue(); diff --git a/core/src/test/java/io/grpc/internal/GrpcUtilTest.java b/core/src/test/java/io/grpc/internal/GrpcUtilTest.java index dbc3051628f..6d0fe33012d 100644 --- a/core/src/test/java/io/grpc/internal/GrpcUtilTest.java +++ b/core/src/test/java/io/grpc/internal/GrpcUtilTest.java @@ -202,6 +202,12 @@ public void checkAuthority_failsOnNull() { assertThrows(NullPointerException.class, () -> GrpcUtil.checkAuthority(null)); } + @Test + public void authorityFromHostAndPort_succeedsOnHostStartingWithDigit() { + String actual = GrpcUtil.authorityFromHostAndPort("otlp.1234-k8s-namespace", 4317); + assertEquals("otlp.1234-k8s-namespace:4317", actual); + } + @Test public void checkAuthority_succeedsOnHostAndPort() { String actual = GrpcUtil.checkAuthority("valid:1234");