This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 18ba732fe0 Issue #7789 : Accept underscores in host names when
building the request origin (#8046)
18ba732fe0 is described below
commit 18ba732fe024322af430e210fc14a2d82f0e016e
Author: vbhanuchander-lang <[email protected]>
AuthorDate: Fri Aug 21 06:26:53 2026 -0400
Issue #7789 : Accept underscores in host names when building the request
origin (#8046)
HttpHost.create(URI) reads URI.getHost(), which java.net.URI leaves null
whenever it treats the authority as registry-based rather than
server-based -- in practice because the host name contains an underscore.
For those URIs getPort() and getUserInfo() are unavailable too:
http://my_service.internal:8080/api
getHost() = null
getPort() = -1
getAuthority() = my_service.internal:8080
so the call fails with NullPointerException: Host name. Four call sites
share it: the HTTP and HTTP POST transforms, the Web Service transform and
HttpProtocol.
Add HttpClientManager.createHttpHost(URI), which uses getHost() when it is
available and otherwise parses the authority -- dropping userinfo, taking
the port when present and leaving IPv6 literals intact -- and route all
four call sites through it.
Underscores are not legal in host names per RFC 1123, but they are common
in internal and container DNS and they resolve, so a NullPointerException
is the wrong outcome.
Note on scope: the proxy configuration already tolerated underscores,
because HttpClientBuilderFacade#setProxy uses the HttpHost(scheme, host,
port) constructor, which does no URI parsing. The reproducible failure is
on the request target, so this fixes that path; asked on the issue for
confirmation of which leg was hit.
Seven tests cover a server-based authority, an underscored host with and
without a port, userinfo removal, an IPv6 literal, a URI with no host and
a non-numeric port. Five of them fail with NullPointerException: Host name
if createHttpHost delegates back to HttpHost.create.
---
.../apache/hop/core/util/HttpClientManager.java | 44 +++++++++++
.../hop/core/util/HttpClientManagerTest.java | 89 ++++++++++++++++++++++
.../java/org/apache/hop/core/HttpProtocol.java | 2 +-
.../apache/hop/pipeline/transforms/http/Http.java | 2 +-
.../hop/pipeline/transforms/httppost/HttpPost.java | 2 +-
.../transforms/webservices/WebService.java | 2 +-
6 files changed, 137 insertions(+), 4 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/util/HttpClientManager.java
b/core/src/main/java/org/apache/hop/core/util/HttpClientManager.java
index 4849c9b1a7..31e200a083 100644
--- a/core/src/main/java/org/apache/hop/core/util/HttpClientManager.java
+++ b/core/src/main/java/org/apache/hop/core/util/HttpClientManager.java
@@ -19,6 +19,7 @@ package org.apache.hop.core.util;
import java.io.FileInputStream;
import java.io.IOException;
+import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
@@ -194,6 +195,49 @@ public class HttpClientManager {
}
}
+ /**
+ * Creates an {@link HttpHost} for the origin of the given URI.
+ *
+ * <p>{@link HttpHost#create(URI)} reads {@link URI#getHost()}, which is
{@code null} whenever the
+ * authority is registry-based rather than server-based -- in practice
because the host name
+ * contains an underscore. Such names are not strictly legal in DNS, but
they are common on
+ * internal networks and resolve perfectly well, and for those URIs {@code
getPort()} and {@code
+ * getUserInfo()} are unavailable too. So parse the authority instead of
letting {@code
+ * HttpHost.create} fail with a NullPointerException.
+ */
+ public static HttpHost createHttpHost(URI uri) {
+ if (uri.getHost() != null) {
+ return new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort());
+ }
+
+ String authority = uri.getAuthority();
+ if (authority == null) {
+ throw new IllegalArgumentException("The URI does not specify a host: " +
uri);
+ }
+
+ // Userinfo is not part of the origin, so drop it.
+ int at = authority.lastIndexOf('@');
+ String hostAndPort = at < 0 ? authority : authority.substring(at + 1);
+
+ String host = hostAndPort;
+ int port = -1;
+ int colon = hostAndPort.lastIndexOf(':');
+ // A colon inside an IPv6 literal is not a port separator.
+ if (colon > -1 && hostAndPort.indexOf(']') < colon) {
+ host = hostAndPort.substring(0, colon);
+ String portText = hostAndPort.substring(colon + 1);
+ if (!portText.isEmpty()) {
+ try {
+ port = Integer.parseInt(portText);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("The URI does not specify a valid
port: " + uri, e);
+ }
+ }
+ }
+
+ return new HttpHost(uri.getScheme(), host, port);
+ }
+
public static SSLContext getSslContextWithTrustStoreFile(
FileInputStream trustFileStream, String trustStorePassword)
throws NoSuchAlgorithmException,
diff --git
a/core/src/test/java/org/apache/hop/core/util/HttpClientManagerTest.java
b/core/src/test/java/org/apache/hop/core/util/HttpClientManagerTest.java
new file mode 100644
index 0000000000..cca7e11dbf
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/util/HttpClientManagerTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.core.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URI;
+import org.apache.hc.core5.http.HttpHost;
+import org.junit.jupiter.api.Test;
+
+class HttpClientManagerTest {
+
+ @Test
+ void createHttpHostReadsAServerBasedAuthority() {
+ HttpHost host =
HttpClientManager.createHttpHost(URI.create("https://example.org:8443/api"));
+
+ assertEquals("https", host.getSchemeName());
+ assertEquals("example.org", host.getHostName());
+ assertEquals(8443, host.getPort());
+ }
+
+ @Test
+ void createHttpHostAcceptsAnUnderscoreInTheHostName() {
+ // java.net.URI treats this authority as registry-based, so getHost(),
getPort() and
+ // getUserInfo() are all unavailable and HttpHost.create(URI) fails with a
NullPointerException.
+ HttpHost host =
+
HttpClientManager.createHttpHost(URI.create("http://my_service.internal:8080/api"));
+
+ assertEquals("http", host.getSchemeName());
+ assertEquals("my_service.internal", host.getHostName());
+ assertEquals(8080, host.getPort());
+ }
+
+ @Test
+ void createHttpHostDefaultsThePortWhenTheAuthorityOmitsIt() {
+ HttpHost host =
HttpClientManager.createHttpHost(URI.create("http://my_service.internal/api"));
+
+ assertEquals("my_service.internal", host.getHostName());
+ assertEquals(-1, host.getPort());
+ }
+
+ @Test
+ void createHttpHostDropsUserInfoFromTheOrigin() {
+ HttpHost host =
+ HttpClientManager.createHttpHost(
+ URI.create("http://user:secret@my_service.internal:8080/"));
+
+ assertEquals("my_service.internal", host.getHostName());
+ assertEquals(8080, host.getPort());
+ }
+
+ @Test
+ void createHttpHostKeepsIpv6LiteralsIntact() {
+ HttpHost host =
HttpClientManager.createHttpHost(URI.create("http://[::1]:8080/api"));
+
+ assertEquals("[::1]", host.getHostName());
+ assertEquals(8080, host.getPort());
+ }
+
+ @Test
+ void createHttpHostRejectsAUriWithoutAHost() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
HttpClientManager.createHttpHost(URI.create("file:///tmp/data.json")));
+ }
+
+ @Test
+ void createHttpHostRejectsANonNumericPort() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
HttpClientManager.createHttpHost(URI.create("http://my_service.internal:http/api")));
+ }
+}
diff --git a/engine/src/main/java/org/apache/hop/core/HttpProtocol.java
b/engine/src/main/java/org/apache/hop/core/HttpProtocol.java
index 70d01c853a..ecd03bb7d6 100644
--- a/engine/src/main/java/org/apache/hop/core/HttpProtocol.java
+++ b/engine/src/main/java/org/apache/hop/core/HttpProtocol.java
@@ -81,7 +81,7 @@ public class HttpProtocol {
HttpClientManager.getInstance().createBuilder();
clientBuilder.setCredentials(username, password);
httpClient = clientBuilder.build();
- HttpHost origin = HttpHost.create(URI.create(urlAsString));
+ HttpHost origin =
HttpClientManager.createHttpHost(URI.create(urlAsString));
HttpClientContext preemptive =
HttpClientUtil.createPreemptiveBasicAuthentication(
origin.getHostName(), origin.getPort(), username, password,
origin.getSchemeName());
diff --git
a/plugins/transforms/http/src/main/java/org/apache/hop/pipeline/transforms/http/Http.java
b/plugins/transforms/http/src/main/java/org/apache/hop/pipeline/transforms/http/Http.java
index 0c6fddfa9a..9d9735ae6a 100644
---
a/plugins/transforms/http/src/main/java/org/apache/hop/pipeline/transforms/http/Http.java
+++
b/plugins/transforms/http/src/main/java/org/apache/hop/pipeline/transforms/http/Http.java
@@ -141,7 +141,7 @@ public class Http extends BaseTransform<HttpMeta, HttpData>
{
// Preemptive Basic auth: AuthCache must be keyed to the origin host
(same as the request
// target). Proxy routing is configured on the client via
RequestConfig — do not pass the
// proxy as HttpHost here (breaks credentials + preemptive cache
matching in HttpClient 5).
- HttpHost target = HttpHost.create(uri);
+ HttpHost target = HttpClientManager.createHttpHost(uri);
httpResponse =
httpClient.execute(
diff --git
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
index a404725244..83e1de6984 100644
---
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
+++
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
@@ -165,7 +165,7 @@ public class HttpPost extends BaseTransform<HttpPostMeta,
HttpPostData> {
long startTime = System.currentTimeMillis();
// Origin host for routing + preemptive Basic auth cache (proxy is on
the client, not here).
- HttpHost target = HttpHost.create(uri);
+ HttpHost target = HttpClientManager.createHttpHost(uri);
HttpClientContext localContext = HttpClientContext.create();
if (StringUtils.isNotBlank(data.realHttpLogin)) {
diff --git
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
index 1d91b3337c..3c5f68e0fd 100644
---
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
+++
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
@@ -565,7 +565,7 @@ public class WebService extends
BaseTransform<WebServiceMeta, WebServiceData> {
CloseableHttpClient httpClient = clientBuilder.build();
if (StringUtils.isNotBlank(login)) {
- HttpHost target = HttpHost.create(URI.create(serviceUrl));
+ HttpHost target =
HttpClientManager.createHttpHost(URI.create(serviceUrl));
AuthCache authCache = new BasicAuthCache();
BasicScheme basicAuth = new BasicScheme();
char[] passwordChars = password != null ? password.toCharArray() : new
char[0];