Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c702446
IPv6 host support
hrishikesh-nalawade Oct 29, 2025
623960f
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Oct 30, 2025
99e1065
IPv6 host support enhancements
hrishikesh-nalawade Nov 5, 2025
93d577c
Merge remote-tracking branch 'origin/hrishikesh-nalawade/GH4318/IPv6-…
hrishikesh-nalawade Nov 5, 2025
0ca2efa
corrections
hrishikesh-nalawade Nov 5, 2025
b1307ad
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 5, 2025
170d4e5
test modifications
hrishikesh-nalawade Nov 7, 2025
a473483
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 7, 2025
cb213d6
test modifications
hrishikesh-nalawade Nov 7, 2025
a3279b3
Merge remote-tracking branch 'origin/hrishikesh-nalawade/GH4318/IPv6-…
hrishikesh-nalawade Nov 7, 2025
f739842
temporarily removing test
hrishikesh-nalawade Nov 7, 2025
2907323
temporarily removing test
hrishikesh-nalawade Nov 7, 2025
7ffa42f
reverting test changes
hrishikesh-nalawade Nov 7, 2025
c01baff
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 10, 2025
a0dc139
code changes
hrishikesh-nalawade Nov 17, 2025
2a5ef3b
added debug logs
hrishikesh-nalawade Nov 17, 2025
f4ad343
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 18, 2025
83d1253
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 20, 2025
a378cb8
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 21, 2025
52580af
Test Changes
hrishikesh-nalawade Nov 27, 2025
c253933
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Nov 27, 2025
fe7c7b9
Removing deprecated code from test
hrishikesh-nalawade Nov 27, 2025
f480f23
Merge remote-tracking branch 'origin/hrishikesh-nalawade/GH4318/IPv6-…
hrishikesh-nalawade Nov 27, 2025
a00a7df
test fix
hrishikesh-nalawade Nov 27, 2025
98d6b2c
Removing strict validation which is causing starUpCheck failure
hrishikesh-nalawade Nov 27, 2025
d2a3ec4
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
achmelo Dec 11, 2025
c409868
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Dec 22, 2025
d4f7a18
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Jan 6, 2026
5cc5b65
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Jan 7, 2026
68040eb
Merge branch 'v3.x.x' into hrishikesh-nalawade/GH4318/IPv6-host-forma…
hrishikesh-nalawade Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.zowe.apiml.apicatalog.model.ApiDocInfo;
import org.zowe.apiml.config.ApiInfo;
import org.zowe.apiml.product.gateway.GatewayClient;
import org.zowe.apiml.util.UrlUtils;
import reactor.core.publisher.Mono;

import java.util.*;
Expand Down Expand Up @@ -78,7 +79,7 @@ springDocProviders, new SpringDocCustomizers(Optional.of(openApiCustomizers), Op
@Override
protected String getServerUrl(ServerHttpRequest serverHttpRequest, String apiDocsUrl) {
var gw = gatewayClient.getGatewayConfigProperties();
return String.format("%s://%s%s", gw.getScheme(), gw.getHostname(), apiDocsUrl);
return UrlUtils.getUrl(gw.getScheme(), gw.getHostname()) + apiDocsUrl;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.zowe.apiml.product.gateway.GatewayClient;
import org.zowe.apiml.product.instance.ServiceAddress;
import org.zowe.apiml.product.routing.RoutedService;
import org.zowe.apiml.util.UrlUtils;

import java.net.URI;
import java.util.Collections;
Expand Down Expand Up @@ -106,7 +107,7 @@ private void updateServer(OpenAPI openAPI) {
if (openAPI.getServers() != null) {
openAPI.getServers()
.forEach(server -> server.setUrl(
String.format("%s://%s/%s", scheme, getHostname(), server.getUrl())));
UrlUtils.getUrl(scheme, UrlUtils.formatHostnameForUrl(getHostname())) + "/" + server.getUrl()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getHostname() returns host+port, which contains ":" even if there is no IPv6 address. This will probably create an incorrect URL

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've considered the possibility that getHostname() might be returning the host along with the port, and I've added handling for that here. But, I suspect the issue might be related to the formatHostnameForUrl() method. I'll take a closer look to better understand what's going wrong.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,77 @@ public boolean isValidUrl(String urlString) {
return false;
}
}

/**
* Formats a hostname properly, ensuring IPv6 addresses are enclosed in square brackets.
* If the input is already a properly formatted IPv6 address (with brackets), it remains unchanged.
*
* @param hostname The hostname or IP address to format
* @return Properly formatted hostname, with IPv6 addresses enclosed in square brackets
*/
public String formatHostnameForUrl(String hostname) {
if (hostname == null) return null;

// If hostname already has IPv6 brackets, don't add them again
if (hostname.startsWith("[") && hostname.contains("]")) {
return hostname;
}

// Check if this is an IPv6 address (contains multiple colons)
if (hostname.contains(":") && !hostname.startsWith("[")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it safe to assume that any address containing ":" is IPv6? Isn't there a better validation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that’s a valid point. I also feel that relying solely on ":" for validation might not be sufficient. I’ll explore better alternatives and see what could work more reliably.

return "[" + hostname + "]";
}

return hostname;
}

/**
* Creates a proper URL string with scheme, hostname, and port,
* handling IPv6 addresses correctly.
*
* @param scheme The URL scheme (http, https, etc.)
* @param hostname The hostname or IP address
* @param port The port number
* @return A properly formatted URL string with IPv6 address handling
*/
public String getUrl(String scheme, String hostname, int port) {
String formattedHostname = formatHostnameForUrl(hostname);
return String.format("%s://%s:%d", scheme, formattedHostname, port);
}

/**
* Creates a proper URL string with scheme and host (which may include port),
* handling IPv6 addresses correctly.
*
* @param scheme The URL scheme (http, https, etc.)
* @param hostWithPort The hostname or IP address, possibly including a port
* @return A properly formatted URL string with IPv6 address handling
*/
public String getUrl(String scheme, String hostWithPort) {
// If host already includes port
if (hostWithPort.contains(":") && !hostWithPort.endsWith("]")) {
// Handle IPv6 address with port
if (hostWithPort.contains("]:")) {
// Already properly formatted IPv6 with port
return String.format("%s://%s", scheme, hostWithPort);
} else if (hostWithPort.contains("[") && hostWithPort.contains("]")) {
// IPv6 without port, just wrap in scheme
return String.format("%s://%s", scheme, hostWithPort);
} else {
// Might be IPv6 without brackets or IPv4 with port
int lastColonIndex = hostWithPort.lastIndexOf(':');
if (hostWithPort.substring(0, lastColonIndex).contains(":")) {
// It's an IPv6 address with port, but without brackets
String host = hostWithPort.substring(0, lastColonIndex);
String port = hostWithPort.substring(lastColonIndex);
return String.format("%s://[%s]%s", scheme, host, port);
}
// IPv4 with port, no special handling needed
return String.format("%s://%s", scheme, hostWithPort);
}
}

// just hostname without port
return String.format("%s://%s", scheme, formatHostnameForUrl(hostWithPort));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;
import org.zowe.apiml.product.gateway.GatewayClient;
import org.zowe.apiml.util.UrlUtils;
import reactor.core.publisher.Mono;

import static reactor.core.publisher.Mono.empty;
Expand Down Expand Up @@ -55,7 +56,10 @@ public CachingServiceClientRest(

void updateUrl() {
// Lazy initialization of GatewayClient's ServerAddress may bring invalid URL during initialization
this.cachingBalancerUrl = String.format("%s://%s/%s", gatewayClient.getGatewayConfigProperties().getScheme(), gatewayClient.getGatewayConfigProperties().getHostname(), CACHING_API_PATH);
this.cachingBalancerUrl = UrlUtils.getUrl(
gatewayClient.getGatewayConfigProperties().getScheme(),
gatewayClient.getGatewayConfigProperties().getHostname()
) + "/" + CACHING_API_PATH;
}

public Mono<Void> create(ApiKeyValue keyValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,26 @@ ServiceAddress gatewayServiceAddress(
) throws URISyntaxException {
if (externalUrl != null) {
URI uri = new URI(externalUrl);
String host = uri.getHost();
// Handle IPv6 address format
if (host != null && host.contains(":")) {
host = "[" + host + "]";
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if host == null, is it ok to continue? it will probably throw some error in the builder

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing that out, I have corrected the above

return ServiceAddress.builder()
.scheme(clientAttlsEnabled ? "http" : uri.getScheme())
.hostname(uri.getHost() + ":" + uri.getPort())
.hostname(host + ":" + uri.getPort())
.build();
}

// Handle IPv6 address format
String formattedHostname = hostname;
if (hostname != null && hostname.contains(":") && !hostname.startsWith("[")) {
formattedHostname = "[" + hostname + "]";
}

return ServiceAddress.builder()
.scheme(determineScheme(serverAttlsEnabled, clientAttlsEnabled, sslEnabled))
.hostname(hostname + ":" + port)
.hostname(formattedHostname + ":" + port)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.zowe.apiml.security.common.error.ServiceNotAccessibleException;
import org.zowe.apiml.ticket.TicketRequest;
import org.zowe.apiml.ticket.TicketResponse;
import org.zowe.apiml.util.UrlUtils;
import org.zowe.apiml.zaas.ZaasTokenResponse;
import reactor.core.publisher.Mono;

Expand Down Expand Up @@ -129,7 +130,8 @@ private WebClient.RequestHeadersSpec<?> createRequest(RequestCredentials request
}

private String getUrl(String pattern, ServiceInstance instance) {
return String.format(pattern, instance.getScheme(), instance.getHost(), instance.getPort(), instance.getServiceId().toLowerCase());
String host = UrlUtils.formatHostnameForUrl(instance.getHost());
return String.format(pattern, instance.getScheme(), host, instance.getPort(), instance.getServiceId().toLowerCase());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.message.yaml.YamlMessageServiceInstance;
import org.zowe.apiml.services.ServiceInfo;
import org.zowe.apiml.util.UrlUtils;
import reactor.core.publisher.Mono;

import java.util.*;
Expand Down Expand Up @@ -67,7 +68,7 @@ public GatewayIndexService(
}

private WebClient buildWebClient(ServiceInstance registration) {
final String baseUrl = String.format("%s://%s:%d", registration.getScheme(), registration.getHost(), registration.getPort());
final String baseUrl = UrlUtils.getUrl(registration.getScheme(), registration.getHost(), registration.getPort());

return webClient.mutate()
.baseUrl(baseUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.util.StringUtils;
import org.zowe.apiml.product.routing.RoutedService;

import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;

Expand Down Expand Up @@ -58,6 +58,31 @@ protected String getHostname(ServiceInstance serviceInstance) {
Map<String, String> metadata = serviceInstance.getMetadata();
if (metadata != null) {
output = metadata.get(SERVICE_EXTERNAL_URL);

// If we have an external URL, ensure IPv6 addresses are properly formatted
if (output != null) {
try {
URI uri = new URI(output);
String host = uri.getHost();

// Check if the host is an IPv6 address (contains colons) and needs brackets
if (host != null && host.contains(":") && !host.startsWith("[")) {
// Rebuild the URI with properly formatted IPv6 address
URI newUri = new URI(
uri.getScheme(),
uri.getUserInfo(),
"[" + host + "]",
uri.getPort(),
uri.getPath(),
uri.getQuery(),
uri.getFragment()
);
output = newUri.toString();
}
} catch (URISyntaxException e) {
// If there's an error parsing the URI, keeping the original URL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some debug log would be good here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, Thank You

}
}
}
if (output == null) {
output = evalHostname(serviceInstance);
Expand All @@ -83,7 +108,46 @@ protected RouteDefinition buildRouteDefinition(ServiceInstance serviceInstance,
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId(serviceInstance.getInstanceId() + ":" + routeId);
routeDefinition.setOrder(getOrder());
routeDefinition.setUri(URI.create(getHostname(serviceInstance)));
String hostname = getHostname(serviceInstance);

// Handle IPv6 formatted URLs properly
if (hostname.startsWith("lb://")) {
// If Load balancer URL format found,keeping it intact
routeDefinition.setUri(URI.create(hostname));
} else {
// For regular URL ensuring proper formatting for IPv6 addresses
try {
URI uri = new URI(hostname);
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
String userInfo = uri.getUserInfo();
String path = uri.getPath();
String query = uri.getQuery();
String fragment = uri.getFragment();

// Checking if the host is an IPv6 address and adding brackets if needed
String formattedHost = host;
if (host != null && host.contains(":") && !host.startsWith("[")) {
formattedHost = "[" + host + "]";
}

// Reconstructing the URI with formatted IPv6 address
URI safeUri = new URI(
scheme,
userInfo,
formattedHost,
port,
path,
query,
fragment
);
routeDefinition.setUri(safeUri);
} catch (URISyntaxException e) {
// Fallback to direct creation if URI parsing fails
routeDefinition.setUri(URI.create(hostname));
}
}

// add instance metadata
routeDefinition.setMetadata(new LinkedHashMap<>(serviceInstance.getMetadata()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.zowe.apiml.product.routing.ServiceType;
import org.zowe.apiml.product.routing.transform.TransformService;
import org.zowe.apiml.product.routing.transform.URLTransformationException;
import org.zowe.apiml.util.UrlUtils;
import org.zowe.apiml.services.ServiceInfo;
import org.zowe.apiml.services.ServiceInfoUtils;

Expand Down Expand Up @@ -92,8 +93,8 @@ public ServiceInfo getServiceInfo(String serviceId) {

private String getBaseUrl(ApiInfo apiInfo, InstanceInfo instanceInfo) {
ServiceAddress gatewayAddress = gatewayClient.getGatewayConfigProperties();
return String.format("%s://%s%s",
gatewayAddress.getScheme(), gatewayAddress.getHostname(), getBasePath(apiInfo, instanceInfo));
return UrlUtils.getUrl(gatewayAddress.getScheme(), gatewayAddress.getHostname()) +
getBasePath(apiInfo, instanceInfo);
}

static List<InstanceInfo> getPrimaryInstances(Application application) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.zowe.apiml.product.gateway.GatewayClient;
import org.zowe.apiml.product.instance.ServiceAddress;
import org.zowe.apiml.security.client.handler.RestResponseHandler;
import org.zowe.apiml.util.UrlUtils;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.error.ErrorType;
import org.zowe.apiml.security.common.login.LoginRequest;
Expand Down Expand Up @@ -58,8 +59,8 @@ public class GatewaySecurityService implements GatewaySecurity {
@Override
public Optional<String> login(String username, char[] password, char[] newPassword) {
ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties();
String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(),
gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayLoginEndpoint());
String uri = UrlUtils.getUrl(gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname()) +
authConfigurationProperties.getGatewayLoginEndpoint();

LoginRequest loginRequest = new LoginRequest(username, password);
if (!ArrayUtils.isEmpty(newPassword)) {
Expand Down Expand Up @@ -95,8 +96,8 @@ public Optional<String> login(String username, char[] password, char[] newPasswo
@Override
public QueryResponse query(String token) {
ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties();
String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(),
gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayQueryEndpoint());
String uri = UrlUtils.getUrl(gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname()) +
authConfigurationProperties.getGatewayQueryEndpoint();
String cookie = String.format("%s=%s", authConfigurationProperties.getCookieProperties().getCookieName(), token);

try {
Expand Down Expand Up @@ -126,8 +127,8 @@ public QueryResponse query(String token) {
@Override
public QueryResponse verifyOidc(String token) {
ServiceAddress gatewayConfigProperties = gatewayClient.getGatewayConfigProperties();
String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(),
gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayOidcValidateEndpoint());
String uri = UrlUtils.getUrl(gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname()) +
authConfigurationProperties.getGatewayOidcValidateEndpoint();

try {
HttpPost post = new HttpPost(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.springframework.web.client.RestTemplate;
import org.zowe.apiml.product.gateway.GatewayClient;
import org.zowe.apiml.product.instance.ServiceAddress;
import org.zowe.apiml.util.UrlUtils;

import java.util.Map;

Expand Down Expand Up @@ -67,7 +68,7 @@ private String getGatewayAddress() {
if (gatewayAddress.getScheme() == null || gatewayAddress.getHostname() == null) {
throw new IllegalStateException("zaasProtocolHostPort has to have value in format <protocol>://<host>:<port> and not be null");
}
return String.format("%s://%s", gatewayAddress.getScheme(), gatewayAddress.getHostname());
return UrlUtils.getUrl(gatewayAddress.getScheme(), gatewayAddress.getHostname());
}

/**
Expand Down
Loading