Issue with Feign client GET request call with query PARAM having special characters are failing with below exception

Spring version : 2.3.4.RELEASE Spring Cloud version : Hoxton.SR8 Java version : open jdk 11 Environment : Kubernates cluster environment with microservices. Cluster setup is with IPV6. All the communiation between microservices via IPV6.

Debugging details:

Kubernates setup with two nodes where local eureka server is configured as discovery server.

Two microservices should be configured with IPV6. Configurations provided below. application.yml server: address: fe80::1126:88ba:243:2db0 port: 8003

eureka: client: serviceUrl: defaultZone: ${EUREKA_URI:http://[fe80::1126:88ba:243:2db0]:8761/eureka} instance: ip-address: fe80::1126:88ba:243:2db0 preferIpAddress: true

JVM arguments to make IPV6 use by application. -Djava.net.preferIPv4Stack=false -Djava.net.preferIPv6Addresses=true

Test case schenario :

Request received from Frontend to one of the microservice say "Service A". Microservice Service A required information from Microservice Service B. To get the data from Microservice Service B, we are makign a Feign Client call from Microservice Service A. Request URL from Service A to Service B is: http://serviceB/api/v1/checkEmailUnique?email=sampleemail@gmail.com

When we make a call, java trace as

Step1 :

BlockingLoadBalancerClient.reconstructURI(BlockingLoadBalancerClient.java:75)

@Override
public URI reconstructURI(ServiceInstance serviceInstance, URI original) {
    return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}

Here serviceInstance has information about the IP address (IPV6 : 240b:c0e0:202:5e2b:b424:2:0:4b4), port (8001) and service name (serviceB). original : http://serviceB/api/v1/checkEmailUnique?email=sampleemail%40gmail.com

Step2 :

/**
 * Modifies the URI in order to redirect the request to a service instance of choice.
 * @param serviceInstance the {@link ServiceInstance} to redirect the request to.
 * @param original the {@link URI} from the original request
 * @return the modified {@link URI}
 */
public static URI reconstructURI(ServiceInstance serviceInstance, URI original) {
    if (serviceInstance == null) {
        throw new IllegalArgumentException("Service Instance cannot be null.");
    }
    return doReconstructURI(serviceInstance, original);
}

Step3 :

private static URI doReconstructURI(ServiceInstance serviceInstance, URI original) {
    String host = serviceInstance.getHost();
    String scheme = Optional.ofNullable(serviceInstance.getScheme())
            .orElse(computeScheme(original, serviceInstance));
    int port = computePort(serviceInstance.getPort(), scheme);

    if (Objects.equals(host, original.getHost()) && port == original.getPort()
            && Objects.equals(scheme, original.getScheme())) {
        return original;
    }

    boolean encoded = containsEncodedParts(original);
    return UriComponentsBuilder.fromUri(original).scheme(scheme).host(host).port(port)
            .build(encoded).toUri();
}

In above code, encoded value is requred as true because url is encloded. boolean encoded = containsEncodedParts(original);

Step 4 :

public UriComponents build(boolean encoded) {
    return buildInternal(encoded ?
            EncodingHint.FULLY_ENCODED :
            this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE);
}

Step 5: EncodingHint.FULLY_ENCODED send

private UriComponents buildInternal(EncodingHint hint) {
    UriComponents result;
    if (this.ssp != null) {
        result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment);
    }
    else {
        HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment,
                this.userInfo, this.host, this.port, this.pathBuilder.build(), this.queryParams,
                hint == EncodingHint.FULLY_ENCODED);

        result = hint == EncodingHint.ENCODE_TEMPLATE ? uric.encodeTemplate(this.charset) : uric;
    }
    if (!this.uriVariables.isEmpty()) {
        result = result.expand(name -> this.uriVariables.getOrDefault(name, UriTemplateVariables.SKIP_VALUE));
    }
    return result;
}

Step 6: Because encoding value is true its calls a method verify()... below...

HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment,
                this.userInfo, this.host, this.port, this.pathBuilder.build(), this.queryParams,
                hint == EncodingHint.FULLY_ENCODED);


/**
 * Package-private constructor. All arguments are optional, and can be {@code null}.
 * @param scheme the scheme
 * @param userInfo the user info
 * @param host the host
 * @param port the port
 * @param path the path
 * @param query the query parameters
 * @param fragment the fragment
 * @param encoded whether the components are already encoded
 */
HierarchicalUriComponents(@Nullable String scheme, @Nullable String fragment, @Nullable String userInfo,
        @Nullable String host, @Nullable String port, @Nullable PathComponent path,
        @Nullable MultiValueMap<String, String> query, boolean encoded) {

    super(scheme, fragment);

    this.userInfo = userInfo;
    this.host = host;
    this.port = port;
    this.path = path != null ? path : NULL_PATH_COMPONENT;
    this.queryParams = query != null ? CollectionUtils.unmodifiableMultiValueMap(query) : EMPTY_QUERY_PARAMS;
    this.encodeState = encoded ? EncodeState.FULLY_ENCODED : EncodeState.RAW;

    // Check for illegal characters..
    if (encoded) {
        verify();
    }
}

Step 7: In verify method. verifyUriComponent(this.host, getHostType());

/**
 * Check if any of the URI components contain any illegal characters.
 * @throws IllegalArgumentException if any component has illegal characters
 */
private void verify() {
    verifyUriComponent(getScheme(), Type.SCHEME);
    verifyUriComponent(this.userInfo, Type.USER_INFO);
    verifyUriComponent(this.host, getHostType());
    this.path.verify();
    this.queryParams.forEach((key, values) -> {
        verifyUriComponent(key, Type.QUERY_PARAM);
        for (String value : values) {
            verifyUriComponent(value, Type.QUERY_PARAM);
        }
    });
    verifyUriComponent(getFragment(), Type.FRAGMENT);
}

Step 8: Here host IP is : 240b:c0e0:202:5e2b:b424:2:0:4b4 and this doesn't have "[" hence its treated as IPV4 address.

private Type getHostType() {
    return (this.host != null && this.host.startsWith("[") ? Type.HOST_IPV6 : Type.HOST_IPV4);
}

Step 9: verifyUriComponent method throws below exception....

verifyUriComponent(this.host, getHostType());

private static void verifyUriComponent(@Nullable String source, Type type) {
    if (source == null) {
        return;
    }
    int length = source.length();
    for (int i = 0; i < length; i++) {
        char ch = source.charAt(i);
        if (ch == '%') {
            if ((i + 2) < length) {
                char hex1 = source.charAt(i + 1);
                char hex2 = source.charAt(i + 2);
                int u = Character.digit(hex1, 16);
                int l = Character.digit(hex2, 16);
                if (u == -1 || l == -1) {
                    throw new IllegalArgumentException("Invalid encoded sequence \"" +
                            source.substring(i) + "\"");
                }
                i += 2;
            }
            else {
                throw new IllegalArgumentException("Invalid encoded sequence \"" +
                        source.substring(i) + "\"");
            }
        }
        else if (!type.isAllowed(ch)) {
            throw new IllegalArgumentException("Invalid character '" + ch + "' for " +
                    type.name() + " in \"" + source + "\"");
        }
    }
}

Exception stack trace:

2020-12-18 08:50:53.488 DEBUG [,bf7c6d79a8c83a34,bf543439eefefbf5,true] 10 --- [r-management-24] c.f.TraceFeignBlockingLoadBalancerClient : Before send
2020-12-18 08:50:53.489 DEBUG [,bf7c6d79a8c83a34,bf543439eefefbf5,true] 10 --- [r-management-24] c.f.TraceFeignBlockingLoadBalancerClient : Exception thrown

java.lang.IllegalArgumentException: Invalid character ':' for HOST_IPV4 in "240b:c0e0:202:5e2b:b424:2:0:4b4"
        at org.springframework.web.util.HierarchicalUriComponents.verifyUriComponent(HierarchicalUriComponents.java:416)
        at org.springframework.web.util.HierarchicalUriComponents.verify(HierarchicalUriComponents.java:379)
        at org.springframework.web.util.HierarchicalUriComponents.<init>(HierarchicalUriComponents.java:145)
        at org.springframework.web.util.UriComponentsBuilder.buildInternal(UriComponentsBuilder.java:417)
        at org.springframework.web.util.UriComponentsBuilder.build(UriComponentsBuilder.java:406)
        at org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.doReconstructURI(LoadBalancerUriTools.java:111)
        at org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.reconstructURI(LoadBalancerUriTools.java:95)
        at org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient.reconstructURI(BlockingLoadBalancerClient.java:75)
        at org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient.execute(FeignBlockingLoadBalancerClient.java:73)
        at org.springframework.cloud.sleuth.instrument.web.client.feign.TraceFeignBlockingLoadBalancerClient.execute(TraceFeignBlockingLoadBalancerClient.java:76)
        at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:119)
        at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89)
        at feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:109)
        at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302)
        at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:298)
        at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46)
        at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
        at rx.Observable.unsafeSubscribe(Observable.java:10327)
        at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:51)
        at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35)
        at rx.Observable.unsafeSubscribe(Observable.java:10327)
        at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:41)
        at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:30)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
        at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
        at rx.Observable.unsafeSubscribe(Observable.java:10327)
        at rx.internal.operators.OperatorSubscribeOn$SubscribeOnSubscriber.call(OperatorSubscribeOn.java:100)
        at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:56)
        at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:47)
        at org.springframework.cloud.sleuth.instrument.async.TraceCallable.call(TraceCallable.java:71)
        at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction.call(HystrixContexSchedulerAction.java:69)
        at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
        at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
        at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)
2020-12-18 08:50:53.490 DEBUG [,bf7c6d79a8c83a34,bf7c6d79a8c83a34,true] 10 --- [nio-8003-exec-2] .m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler com.mavenir.dep.accountmanagement.exception.CustomResponseEntityExceptionHandler#handleHystrixRuntimeException(HystrixRuntimeException, WebRequest)
2020-12-18 08:50:53.491 ERROR [,bf7c6d79a8c83a34,bf7c6d79a8c83a34,true] 10 --- [nio-8003-exec-2] a.e.CustomResponseEntityExceptionHandler : Hystrix Runtime Exception caused by :

Comment From: rstoyanchev

I've edited your comment to improve the formatting. You might want to check out this Mastering Markdown guide for future reference. Especially when posting a very long description, increase your chances of getting help by using eligible formatting.

Comment From: rstoyanchev

According to the URI spec the syntax for IPV6 requires surrounding "[" and "]" and therefore HierarchicalUriComponents is correct to reject it.

This could be an issue of application misconfiguration or it could be a bug in https://github.com/spring-cloud/spring-cloud-openfeign, I'm not sure which one, but there is nothing I can see that we can solve at the Spring Framework level.

Comment From: vasureddys

Thank you and please close this ticket. I will follow with Spring cloud openfeign.