From b7b359422fa50abb8fb45ad390ac7449a1d2c05c Mon Sep 17 00:00:00 2001 From: Hitesh Kalluru Date: Wed, 23 Sep 2026 22:16:36 -0400 Subject: [PATCH 1/4] Fail fast in JDK and grpc-java senders for hosts java.net.URI cannot parse JdkHttpSender and UpstreamGrpcSenderProvider's fallback channel rely on java.net.URI to parse the endpoint host, which returns null for valid DNS names whose last label starts with a digit (JDK-8188305). EndpointUtil accepts these since #8746, so they previously failed with an opaque error on first export or a NullPointerException. Throw a clear error at sender construction instead. Part of #8745 --- .../internal/UpstreamGrpcSenderProvider.java | 9 +++ .../UpstreamGrpcSenderProviderTest.java | 53 ++++++++++++++++ .../jdk/internal/JdkHttpSenderProvider.java | 14 ++++- .../internal/JdkHttpSenderProviderTest.java | 60 +++++++++++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 exporters/sender/grpc-managed-channel/src/test/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProviderTest.java create mode 100644 exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProviderTest.java diff --git a/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java b/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java index a897a844611..912b09bc6a0 100644 --- a/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java +++ b/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java @@ -67,6 +67,15 @@ public GrpcSender createSender(GrpcSenderConfig grpcSenderConfig) { * {@link ManagedChannel}. */ private static ManagedChannel minimalFallbackManagedChannel(URI endpoint) { + // grpc-java parses host names with java.net.URI, which returns null for some valid DNS names + // (JDK-8188305) that EndpointUtil accepts, so fail with a clear message. + if (endpoint.getHost() == null) { + throw new IllegalArgumentException( + "Invalid endpoint, host not supported by grpc-java: " + + endpoint + + ". java.net.URI cannot parse host names whose last label starts with a digit." + + " Use the OkHttp sender or a fully qualified host name."); + } ManagedChannelBuilder channelBuilder = ManagedChannelBuilder.forAddress(endpoint.getHost(), endpoint.getPort()); if (!endpoint.getScheme().equals("https")) { diff --git a/exporters/sender/grpc-managed-channel/src/test/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProviderTest.java b/exporters/sender/grpc-managed-channel/src/test/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProviderTest.java new file mode 100644 index 00000000000..e9ee10b79e7 --- /dev/null +++ b/exporters/sender/grpc-managed-channel/src/test/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProviderTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.sender.grpc.managedchannel.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.grpc.ManagedChannel; +import io.opentelemetry.exporter.internal.grpc.ExtendedGrpcSenderConfig; +import java.net.URI; +import java.time.Duration; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class UpstreamGrpcSenderProviderTest { + + @ParameterizedTest + @ValueSource( + strings = {"http://otlp.1234-k8s-namespace:4317", "https://collector.9namespace:4317"}) + void createSender_FallbackChannel_HostUnparseableByUri_Throws(String endpoint) { + ExtendedGrpcSenderConfig config = config(endpoint); + + assertThatThrownBy(() -> new UpstreamGrpcSenderProvider().createSender(config)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("host not supported by grpc-java: " + endpoint); + } + + @Test + void createSender_ExplicitChannel_HostUnparseableByUri_Succeeds() { + // The endpoint is not used to build an explicitly set channel. + ExtendedGrpcSenderConfig config = config("http://otlp.1234-k8s-namespace:4317"); + when(config.getManagedChannel()).thenReturn(mock(ManagedChannel.class)); + + assertThat(new UpstreamGrpcSenderProvider().createSender(config)).isNotNull(); + } + + private static ExtendedGrpcSenderConfig config(String endpoint) { + ExtendedGrpcSenderConfig config = mock(ExtendedGrpcSenderConfig.class); + when(config.getEndpoint()).thenReturn(URI.create(endpoint)); + when(config.getFullMethodName()) + .thenReturn("opentelemetry.proto.collector.trace.v1.TraceService/Export"); + when(config.getTimeout()).thenReturn(Duration.ofSeconds(10)); + when(config.getHeadersSupplier()).thenReturn(Collections::emptyMap); + return config; + } +} diff --git a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java index 1a6c0948893..936540083f2 100644 --- a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java +++ b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java @@ -8,6 +8,7 @@ import io.opentelemetry.sdk.common.export.HttpSender; import io.opentelemetry.sdk.common.export.HttpSenderConfig; import io.opentelemetry.sdk.common.export.HttpSenderProvider; +import java.net.URI; /** * {@link HttpSender} SPI implementation for {@link JdkHttpSender}. @@ -19,8 +20,19 @@ public final class JdkHttpSenderProvider implements HttpSenderProvider { @Override public HttpSender createSender(HttpSenderConfig httpSenderConfig) { + URI endpoint = httpSenderConfig.getEndpoint(); + // HttpRequest.Builder#uri rejects a URI without a host. URI#getHost() returns null for some + // valid DNS names (JDK-8188305), which EndpointUtil accepts, so fail here with a clear message + // instead of on the first export. + if (endpoint.getHost() == null) { + throw new IllegalArgumentException( + "Invalid endpoint, host not supported by the JDK HttpClient: " + + endpoint + + ". java.net.URI cannot parse host names whose last label starts with a digit." + + " Use the OkHttp sender or a fully qualified host name."); + } return new JdkHttpSender( - httpSenderConfig.getEndpoint(), + endpoint, httpSenderConfig.getContentType(), httpSenderConfig.getCompressor(), httpSenderConfig.getTimeout(), diff --git a/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProviderTest.java b/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProviderTest.java new file mode 100644 index 00000000000..5921bab4f12 --- /dev/null +++ b/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProviderTest.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.sender.jdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.sdk.common.export.HttpSender; +import io.opentelemetry.sdk.common.export.HttpSenderConfig; +import java.net.URI; +import java.time.Duration; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class JdkHttpSenderProviderTest { + + @ParameterizedTest + @ValueSource( + strings = { + "http://otlp.1234-k8s-namespace:4318/v1/traces", + "https://collector.9namespace:4318/v1/traces", + }) + void createSender_HostUnparseableByUri_Throws(String endpoint) { + HttpSenderConfig config = config(endpoint); + + assertThatThrownBy(() -> new JdkHttpSenderProvider().createSender(config)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("host not supported by the JDK HttpClient: " + endpoint); + } + + @ParameterizedTest + @ValueSource( + strings = { + "http://localhost:4318/v1/traces", + "http://otlp.1234-k8s-namespace.svc.cluster.local:4318/v1/traces", + }) + void createSender_HostParseableByUri_Succeeds(String endpoint) { + HttpSender sender = new JdkHttpSenderProvider().createSender(config(endpoint)); + + assertThat(sender.shutdown().join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + } + + private static HttpSenderConfig config(String endpoint) { + HttpSenderConfig config = mock(HttpSenderConfig.class); + when(config.getEndpoint()).thenReturn(URI.create(endpoint)); + when(config.getContentType()).thenReturn("application/x-protobuf"); + when(config.getTimeout()).thenReturn(Duration.ofSeconds(10)); + when(config.getConnectTimeout()).thenReturn(Duration.ofSeconds(10)); + when(config.getHeadersSupplier()).thenReturn(Collections::emptyMap); + when(config.getMaxResponseBodySize()).thenReturn(Long.MAX_VALUE); + return config; + } +} From 2a0b96093b60523ff07f67075b65ba1bfbf12d07 Mon Sep 17 00:00:00 2001 From: hitkall <167158130+hitkall@users.noreply.github.com> Date: Fri, 13 Nov 2026 01:22:37 -0500 Subject: [PATCH 2/4] Reference upstream grpc-java issue in fallback host validation comment --- .../1-UpstreamGrpcSenderProvider.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java diff --git a/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java b/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java new file mode 100644 index 00000000000..4d6e7231c49 --- /dev/null +++ b/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.sender.grpc.managedchannel.internal; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.exporter.internal.grpc.ExtendedGrpcSenderConfig; +import io.opentelemetry.sdk.common.export.GrpcSender; +import io.opentelemetry.sdk.common.export.GrpcSenderConfig; +import io.opentelemetry.sdk.common.export.GrpcSenderProvider; +import java.net.URI; + +/** + * {@link GrpcSender} SPI implementation for {@link UpstreamGrpcSender}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class UpstreamGrpcSenderProvider implements GrpcSenderProvider { + + @Override + public GrpcSender createSender(GrpcSenderConfig grpcSenderConfig) { + if (!(grpcSenderConfig instanceof ExtendedGrpcSenderConfig)) { + throw new IllegalStateException( + "grpcSenderConfig must be an instance of ExtendedGrpcSenderConfig for UpstreamGrpcSenderProvider"); + } + ExtendedGrpcSenderConfig extendedSenderConfig = (ExtendedGrpcSenderConfig) grpcSenderConfig; + + boolean shutdownChannel = false; + Object configManagedChannel = extendedSenderConfig.getManagedChannel(); + ManagedChannel managedChannel; + if (configManagedChannel != null) { + if (!(configManagedChannel instanceof ManagedChannel)) { + throw new IllegalStateException( + "managedChannel must be an instance of ManagedChannel for UpstreamGrpcSenderProvider"); + } + managedChannel = (ManagedChannel) configManagedChannel; + } else { + // Shutdown the channel as part of the exporter shutdown sequence if we created it. + shutdownChannel = true; + managedChannel = minimalFallbackManagedChannel(grpcSenderConfig.getEndpoint()); + } + + return new UpstreamGrpcSender( + managedChannel, + extendedSenderConfig.getFullMethodName(), + extendedSenderConfig.getCompressor(), + shutdownChannel, + extendedSenderConfig.getTimeout(), + extendedSenderConfig.getHeadersSupplier(), + extendedSenderConfig.getExecutorService()); + } + + /** + * If {@link ManagedChannel} is not explicitly set, provide a minimally configured fallback + * channel to avoid failing initialization. + * + *

This is required to accommodate autoconfigure with {@code + * opentelemetry-exporter-sender-grpc-managed-channel} which will always fail to initialize + * without a fallback channel since there isn't an opportunity to explicitly set the channel. + * + *

This only incorporates the target address, port, and whether to use plain text. All + * additional settings are intentionally ignored and must be configured with an explicitly set + * {@link ManagedChannel}. + */ + private static ManagedChannel minimalFallbackManagedChannel(URI endpoint) { + // grpc-java parses host names with java.net.URI, which returns null for some valid DNS names + // (JDK-8188305) that EndpointUtil accepts, so fail with a clear message. Filed upstream as + // https://github.com/grpc/grpc-java/issues/13075; this check can be relaxed if grpc-java + // fixes host validation on their side. + if (endpoint.getHost() == null) { + throw new IllegalArgumentException( + "Invalid endpoint, host not supported by grpc-java: " + + endpoint + + ". java.net.URI cannot parse host names whose last label starts with a digit." + + " Use the OkHttp sender or a fully qualified host name."); + } + ManagedChannelBuilder channelBuilder = + ManagedChannelBuilder.forAddress(endpoint.getHost(), endpoint.getPort()); + if (!endpoint.getScheme().equals("https")) { + channelBuilder.usePlaintext(); + } + return channelBuilder.build(); + } +} From 6347402ca72ec8c6ebf766877acc59d5428e5558 Mon Sep 17 00:00:00 2001 From: hitkall <167158130+hitkall@users.noreply.github.com> Date: Fri, 13 Nov 2026 01:24:31 -0500 Subject: [PATCH 3/4] Delete exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java --- .../1-UpstreamGrpcSenderProvider.java | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java diff --git a/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java b/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java deleted file mode 100644 index 4d6e7231c49..00000000000 --- a/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/1-UpstreamGrpcSenderProvider.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.sender.grpc.managedchannel.internal; - -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.opentelemetry.exporter.internal.grpc.ExtendedGrpcSenderConfig; -import io.opentelemetry.sdk.common.export.GrpcSender; -import io.opentelemetry.sdk.common.export.GrpcSenderConfig; -import io.opentelemetry.sdk.common.export.GrpcSenderProvider; -import java.net.URI; - -/** - * {@link GrpcSender} SPI implementation for {@link UpstreamGrpcSender}. - * - *

This class is internal and is hence not for public use. Its APIs are unstable and can change - * at any time. - */ -public class UpstreamGrpcSenderProvider implements GrpcSenderProvider { - - @Override - public GrpcSender createSender(GrpcSenderConfig grpcSenderConfig) { - if (!(grpcSenderConfig instanceof ExtendedGrpcSenderConfig)) { - throw new IllegalStateException( - "grpcSenderConfig must be an instance of ExtendedGrpcSenderConfig for UpstreamGrpcSenderProvider"); - } - ExtendedGrpcSenderConfig extendedSenderConfig = (ExtendedGrpcSenderConfig) grpcSenderConfig; - - boolean shutdownChannel = false; - Object configManagedChannel = extendedSenderConfig.getManagedChannel(); - ManagedChannel managedChannel; - if (configManagedChannel != null) { - if (!(configManagedChannel instanceof ManagedChannel)) { - throw new IllegalStateException( - "managedChannel must be an instance of ManagedChannel for UpstreamGrpcSenderProvider"); - } - managedChannel = (ManagedChannel) configManagedChannel; - } else { - // Shutdown the channel as part of the exporter shutdown sequence if we created it. - shutdownChannel = true; - managedChannel = minimalFallbackManagedChannel(grpcSenderConfig.getEndpoint()); - } - - return new UpstreamGrpcSender( - managedChannel, - extendedSenderConfig.getFullMethodName(), - extendedSenderConfig.getCompressor(), - shutdownChannel, - extendedSenderConfig.getTimeout(), - extendedSenderConfig.getHeadersSupplier(), - extendedSenderConfig.getExecutorService()); - } - - /** - * If {@link ManagedChannel} is not explicitly set, provide a minimally configured fallback - * channel to avoid failing initialization. - * - *

This is required to accommodate autoconfigure with {@code - * opentelemetry-exporter-sender-grpc-managed-channel} which will always fail to initialize - * without a fallback channel since there isn't an opportunity to explicitly set the channel. - * - *

This only incorporates the target address, port, and whether to use plain text. All - * additional settings are intentionally ignored and must be configured with an explicitly set - * {@link ManagedChannel}. - */ - private static ManagedChannel minimalFallbackManagedChannel(URI endpoint) { - // grpc-java parses host names with java.net.URI, which returns null for some valid DNS names - // (JDK-8188305) that EndpointUtil accepts, so fail with a clear message. Filed upstream as - // https://github.com/grpc/grpc-java/issues/13075; this check can be relaxed if grpc-java - // fixes host validation on their side. - if (endpoint.getHost() == null) { - throw new IllegalArgumentException( - "Invalid endpoint, host not supported by grpc-java: " - + endpoint - + ". java.net.URI cannot parse host names whose last label starts with a digit." - + " Use the OkHttp sender or a fully qualified host name."); - } - ManagedChannelBuilder channelBuilder = - ManagedChannelBuilder.forAddress(endpoint.getHost(), endpoint.getPort()); - if (!endpoint.getScheme().equals("https")) { - channelBuilder.usePlaintext(); - } - return channelBuilder.build(); - } -} From e283b88a441cc518072ee61729f03f2646222fc3 Mon Sep 17 00:00:00 2001 From: hitkall <167158130+hitkall@users.noreply.github.com> Date: Fri, 13 Nov 2026 01:25:59 -0500 Subject: [PATCH 4/4] Reference upstream grpc-java issue in fallback host validation comment --- .../managedchannel/internal/UpstreamGrpcSenderProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java b/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java index 912b09bc6a0..4d6e7231c49 100644 --- a/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java +++ b/exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/UpstreamGrpcSenderProvider.java @@ -68,7 +68,9 @@ public GrpcSender createSender(GrpcSenderConfig grpcSenderConfig) { */ private static ManagedChannel minimalFallbackManagedChannel(URI endpoint) { // grpc-java parses host names with java.net.URI, which returns null for some valid DNS names - // (JDK-8188305) that EndpointUtil accepts, so fail with a clear message. + // (JDK-8188305) that EndpointUtil accepts, so fail with a clear message. Filed upstream as + // https://github.com/grpc/grpc-java/issues/13075; this check can be relaxed if grpc-java + // fixes host validation on their side. if (endpoint.getHost() == null) { throw new IllegalArgumentException( "Invalid endpoint, host not supported by grpc-java: "