Repository: ably/ably-java
Issue: Realtime with authUrl with token in connection string fails to connect

See failing https://github.com/ably/ably-java/actions/runs/4882297437/jobs/8712194485

When authUrl contains token in query string, realtime does not connect

Changed files:
lib/src/main/java/io/ably/lib/rest/Auth.java, lib/src/main/java/io/ably/lib/http/HttpUtils.java, lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java

diff --git a/lib/src/main/java/io/ably/lib/http/HttpUtils.java b/lib/src/main/java/io/ably/lib/http/HttpUtils.java
index 02889449..7852f375 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpUtils.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpUtils.java
@@ -4,6 +4,8 @@ import com.google.gson.JsonElement;
 
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
@@ -78,6 +80,28 @@ public class HttpUtils {
         }
     }
 
+    /**
+     * Removes querystring from given url string and returns the url string without query string(s)
+     * @param url Url string that needs querystring part removed
+     *
+     * @return  Url string with query string part removed, if existed in the first place
+     *
+     * @throws AblyException  built from URISyntaxException if java.net.URI fails to build
+     * the URI given url
+     * */
+    public static String urlWithQueryStringRemoved(String url) throws AblyException {
+        try {
+            final URI uri = new URI(url);
+            return new URI(uri.getScheme(),
+                uri.getAuthority(),
+                uri.getPath(),
+                null, // Ignore the query part of the input url
+                uri.getFragment()).toString();
+        } catch (URISyntaxException e) {
+            throw AblyException.fromThrowable(e);
+        }
+    }
+
     public static Map<String, Param> decodeParams(String query) {
         Map<String, Param> params = new HashMap<String, Param>();
         String[] pairs = query.split("&");
diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java
index dc3fa9fc..7a8d0a5d 100644
--- a/lib/src/main/java/io/ably/lib/rest/Auth.java
+++ b/lib/src/main/java/io/ably/lib/rest/Auth.java
@@ -805,6 +805,7 @@ public class Auth {
                 /* append all relevant params to token params */
                 Map<String, Param> urlParams = null;
                 URL authUrl = HttpUtils.parseUrl(authOptions.authUrl);
+                final String urlWithoutQueryParams = HttpUtils.urlWithQueryStringRemoved(authOptions.authUrl);
                 String queryString = authUrl.getQuery();
                 if(queryString != null && !queryString.isEmpty()) {
                     urlParams = HttpUtils.decodeParams(queryString);
@@ -820,10 +821,12 @@ public class Auth {
                     }
                 }
                 if (HttpConstants.Methods.POST.equals(tokenOptions.authMethod)) {
-                    authUrlResponse = HttpHelpers.postUri(ably.httpCore, tokenOptions.authUrl, tokenOptions.authHeaders, HttpUtils.flattenParams(urlParams), HttpUtils.flattenParams(tokenParams), responseHandler);
+                    authUrlResponse = HttpHelpers.postUri(ably.httpCore, urlWithoutQueryParams, tokenOptions.authHeaders,
+                        HttpUtils.flattenParams(urlParams), HttpUtils.flattenParams(tokenParams), responseHandler);
                 } else {
                     Map<String, Param> requestParams = (urlParams != null) ? HttpUtils.mergeParams(urlParams, tokenParams) : tokenParams;
-                    authUrlResponse = HttpHelpers.getUri(ably.httpCore, tokenOptions.authUrl, tokenOptions.authHeaders, HttpUtils.flattenParams(requestParams), responseHandler);
+                    authUrlResponse = HttpHelpers.getUri(ably.httpCore, urlWithoutQueryParams, tokenOptions.authHeaders,
+                        HttpUtils.flattenParams(requestParams), responseHandler);
                 }
             } catch(AblyException e) {
                 throw AblyException.fromErrorInfo(e, new ErrorInfo("authUrl failed with an exception", e.errorInfo.statusCode, 80019));
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
index 7b2bab0a..c8950eed 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
@@ -34,6 +34,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.net.URLEncoder;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -97,6 +98,40 @@ public class RealtimeAuthTest extends ParameterizedTest {
         }
     }
 
+    /**
+     * Given authUrl in the form of query string,ensure that realtime will connect without any problem
+     */
+    @Test
+    public void realtime_connection_with_auth_url_in_query_string_connects() {
+        try {
+            /* init ably for token */
+            ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr);
+            final AblyRest ablyForToken = new AblyRest(optsForToken);
+
+            /* get token */
+            Auth.TokenParams tokenParams = new Auth.TokenParams();
+            Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null);
+            assertNotNull("Expected token value", tokenDetails.token);
+
+            /* create ably realtime with tokenDetails and clientId */
+            ClientOptions opts = createOptions();
+            opts.authUrl = "https://echo.ably.io/?body="+ URLEncoder.encode(tokenDetails.token);
+            opts.useTokenAuth = true;
+            AblyRealtime ablyRealtime = new AblyRealtime(opts);
+            System.out.println("done create ably");
+
+            /* wait for connected state */
+            Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection);
+            connectionWaiter.waitFor(ConnectionState.connected);
+            assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state);
+
+            ablyRealtime.close();
+        } catch (AblyException e) {
+            e.printStackTrace();
+            fail();
+        }
+    }
+
     /**
      * RSA4d: If a request by a realtime client to an authUrl results in an HTTP 403 response,
      * or any of an authUrl request, an authCallback, or a request to Ably to exchange