This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 27b8abbb6eb14f52cd49f055bee61179d342021f
Author: David Smiley <[email protected]>
AuthorDate: Sun Apr 12 11:37:45 2026 -0400

    SOLR-18188: RestTestHarness overhaul, add URLUtil.buildURI (#4270)
    
    Refactors RestTestHarness to be immutable and use Jetty HttpClient instead 
of Apache HttpClient.
    Introduces URLUtil.buildURI helper, used by RestTestHarness and a backup 
test.
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 solr/core/build.gradle                             |   1 -
 .../apache/solr/core/TestConfigSetImmutable.java   |   5 -
 .../org/apache/solr/core/TestCustomStream.java     |   4 -
 .../solr/core/TestSetPropertyConfigApis.java       |  19 +-
 .../apache/solr/core/TestSolrConfigHandler.java    | 295 +++++----------------
 .../org/apache/solr/handler/TestReqParamsAPI.java  |  76 ++----
 .../solr/handler/TestSolrConfigHandlerCloud.java   |  48 +---
 .../handler/TestSolrConfigHandlerConcurrent.java   |  53 ++--
 .../test/org/apache/solr/rest/TestRestManager.java |   5 +
 .../apache/solr/rest/schema/TestBulkSchemaAPI.java |   4 -
 .../analysis/TestManagedStopFilterFactory.java     |   5 -
 .../TestManagedSynonymGraphFilterFactory.java      |   5 -
 .../solr/schema/TestUseDocValuesAsStored2.java     |   5 -
 .../solr/languagemodels/TestLanguageModelBase.java |   5 +-
 .../store/rest/TestModelManagerPersistence.java    |   6 +-
 .../org/apache/solr/ltr/TestLTROnSolrCloud.java    |   5 +-
 .../test/org/apache/solr/ltr/TestRerankBase.java   |   9 +-
 .../store/rest/TestModelManagerPersistence.java    |   9 +-
 .../java/org/apache/solr/common/util/URLUtil.java  | 106 ++++++++
 .../solr/client/solrj/request/SchemaTest.java      |   4 -
 .../org/apache/solr/common/util/URLUtilTest.java   | 156 +++++++++++
 .../solr/cloud/AbstractFullDistribZkTestBase.java  |  10 +-
 .../org/apache/solr/embedded/JettySolrRunner.java  |  33 ++-
 .../apache/solr/handler/BackupRestoreUtils.java    |  52 ++--
 .../java/org/apache/solr/util/RestTestBase.java    |  33 +--
 .../java/org/apache/solr/util/RestTestHarness.java | 206 ++++++++------
 26 files changed, 584 insertions(+), 575 deletions(-)

diff --git a/solr/core/build.gradle b/solr/core/build.gradle
index f1bb21be4aa..819ad44f645 100644
--- a/solr/core/build.gradle
+++ b/solr/core/build.gradle
@@ -203,7 +203,6 @@ dependencies {
     exclude group: "net.bytebuddy", module: "byte-buddy-agent"
   })
   testImplementation libs.apache.httpcomponents.httpclient
-  testImplementation libs.apache.httpcomponents.httpcore
 
   testImplementation libs.opentelemetry.sdk.testing
   testImplementation libs.dropwizard.metrics.core
diff --git 
a/solr/core/src/test/org/apache/solr/core/TestConfigSetImmutable.java 
b/solr/core/src/test/org/apache/solr/core/TestConfigSetImmutable.java
index 78719fe9fe4..4d456a2f826 100644
--- a/solr/core/src/test/org/apache/solr/core/TestConfigSetImmutable.java
+++ b/solr/core/src/test/org/apache/solr/core/TestConfigSetImmutable.java
@@ -61,11 +61,6 @@ public class TestConfigSetImmutable extends RestTestBase {
   @After
   public void after() throws Exception {
     solrTestRule.reset();
-
-    if (restTestHarness != null) {
-      restTestHarness.close();
-    }
-    restTestHarness = null;
   }
 
   @Test
diff --git a/solr/core/src/test/org/apache/solr/core/TestCustomStream.java 
b/solr/core/src/test/org/apache/solr/core/TestCustomStream.java
index 96fc61f165f..73c8afc93de 100644
--- a/solr/core/src/test/org/apache/solr/core/TestCustomStream.java
+++ b/solr/core/src/test/org/apache/solr/core/TestCustomStream.java
@@ -36,18 +36,14 @@ public class TestCustomStream extends 
AbstractFullDistribZkTestBase {
     TestSolrConfigHandler.runConfigCommand(client, "/config", payload);
     TestSolrConfigHandler.testForResponseElement(
         client,
-        null,
         "/config/overlay",
-        null,
         Arrays.asList("overlay", "expressible", "hello", "class"),
         "org.apache.solr.core.HelloStream",
         10);
 
     TestSolrConfigHandler.testForResponseElement(
         client,
-        null,
         "/stream?expr=hello()",
-        null,
         Arrays.asList("result-set", "docs[0]", "msg"),
         "Hello World!",
         10);
diff --git 
a/solr/core/src/test/org/apache/solr/core/TestSetPropertyConfigApis.java 
b/solr/core/src/test/org/apache/solr/core/TestSetPropertyConfigApis.java
index eb60c3f8347..54c30f7d75f 100644
--- a/solr/core/src/test/org/apache/solr/core/TestSetPropertyConfigApis.java
+++ b/solr/core/src/test/org/apache/solr/core/TestSetPropertyConfigApis.java
@@ -290,15 +290,13 @@ public class TestSetPropertyConfigApis extends 
SolrCloudTestCase {
   private static void setUserProperties(
       final String collectionName, final Map<String, String> props) throws 
Exception {
 
-    try (RestTestHarness harness = makeRestHarness(collectionName)) {
-      final String cmd =
-          "{ 'set-user-property' : { "
-              + props.entrySet().stream()
-                  .map(e -> "'" + e.getKey() + "':'" + e.getValue() + "'")
-                  .collect(Collectors.joining(","))
-              + "}} ";
-      runConfigCommand(harness, cmd);
-    }
+    final String cmd =
+        "{ 'set-user-property' : { "
+            + props.entrySet().stream()
+                .map(e -> "'" + e.getKey() + "':'" + e.getValue() + "'")
+                .collect(Collectors.joining(","))
+            + "}} ";
+    runConfigCommand(makeRestHarness(collectionName), cmd);
   }
 
   /**
@@ -345,8 +343,7 @@ public class TestSetPropertyConfigApis extends 
SolrCloudTestCase {
   }
 
   private static RestTestHarness makeRestHarness(final String collectionName) {
-    return new RestTestHarness(
-        () -> cluster.getRandomJetty(random()).getBaseUrl().toString() + "/" + 
collectionName);
+    return cluster.getRandomJetty(random()).getRestClient(collectionName);
   }
 
   private static void runConfigCommand(RestTestHarness harness, String json) 
throws IOException {
diff --git a/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java 
b/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java
index 8649327aa60..4cb5b8889b6 100644
--- a/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java
+++ b/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java
@@ -33,7 +33,6 @@ import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.io.file.PathUtils;
 import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.common.LinkedHashMapWriter;
 import org.apache.solr.common.MapWriter;
 import org.apache.solr.common.util.StrUtils;
@@ -41,11 +40,9 @@ import org.apache.solr.common.util.TimeSource;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.common.util.ValidatingJsonMap;
 import org.apache.solr.handler.DumpRequestHandler;
-import org.apache.solr.handler.TestSolrConfigHandlerConcurrent;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.search.SolrCache;
-import org.apache.solr.util.RESTfulServerProvider;
 import org.apache.solr.util.RestTestBase;
 import org.apache.solr.util.RestTestHarness;
 import org.apache.solr.util.TimeOut;
@@ -91,24 +88,19 @@ public class TestSolrConfigHandler extends RestTestBase {
     createJettyAndHarness(tmpSolrHome, "solrconfig-managed-schema.xml", 
"schema-rest.xml");
     if (random().nextBoolean()) {
       log.info("These tests are run with V2 API");
-      restTestHarness.setServerProvider(
-          () -> getBaseUrl() + "/____v2/cores/" + DEFAULT_TEST_CORENAME);
+      restTestHarness =
+          restTestHarness.newWithUrl(getBaseUrl() + "/____v2/cores/" + 
DEFAULT_TEST_CORENAME);
     }
   }
 
   @After
   public void after() throws Exception {
     solrTestRule.reset();
-
-    if (restTestHarness != null) {
-      restTestHarness.close();
-    }
-    restTestHarness = null;
   }
 
   public void testProperty() throws Exception {
     RestTestHarness harness = restTestHarness;
-    MapWriter confMap = getRespMap("/config", harness);
+    MapWriter confMap = getRespMap(harness, "/config");
     assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/luke"), null));
     assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/info"), null));
     assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/file"), null));
@@ -120,16 +112,16 @@ public class TestSolrConfigHandler extends RestTestBase {
             + " }";
     runConfigCommand(harness, "/config", payload);
 
-    MapWriter m = getRespMap("/config/overlay", harness);
+    MapWriter m = getRespMap(harness, "/config/overlay");
     assertEquals("100", 
m._getStr("overlay/props/updateHandler/autoCommit/maxDocs"));
     assertEquals("10", 
m._getStr("overlay/props/updateHandler/autoCommit/maxTime"));
 
-    m = getRespMap("/config/updateHandler", harness);
+    m = getRespMap(harness, "/config/updateHandler");
     assertNotNull(m._get("config/updateHandler/commitWithin/softCommit"));
     assertNotNull(m._get("config/updateHandler/autoCommit/maxDocs"));
     assertNotNull(m._get("config/updateHandler/autoCommit/maxTime"));
 
-    m = getRespMap("/config", harness);
+    m = getRespMap(harness, "/config");
     assertNotNull(m);
 
     assertEquals("100", m._getStr("config/updateHandler/autoCommit/maxDocs"));
@@ -138,7 +130,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     payload = "{\n" + " 'unset-property' :  'updateHandler.autoCommit.maxDocs' 
\n" + " }";
     runConfigCommand(harness, "/config", payload);
 
-    m = getRespMap("/config/overlay", harness);
+    m = getRespMap(harness, "/config/overlay");
     assertNull(m._get("overlay/props/updateHandler/autoCommit/maxDocs"));
     assertEquals("10", 
m._getStr("overlay/props/updateHandler/autoCommit/maxTime"));
   }
@@ -152,18 +144,18 @@ public class TestSolrConfigHandler extends RestTestBase {
             + " }";
     runConfigCommand(harness, "/config", payload);
 
-    MapWriter m = getRespMap("/config/overlay", harness); // .get("overlay");
+    MapWriter m = getRespMap(harness, "/config/overlay"); // .get("overlay");
     assertEquals(m._get("overlay/userProps/my.custom.variable.a"), 
"MODIFIEDA");
     assertEquals(m._get("overlay/userProps/my.custom.variable.b"), 
"MODIFIEDB");
 
-    m = getRespMap("/dump?json.nl=map&initArgs=true", harness); // 
.get("initArgs");
+    m = getRespMap(harness, "/dump?json.nl=map&initArgs=true"); // 
.get("initArgs");
 
     assertEquals("MODIFIEDA", m._get("initArgs/defaults/a"));
     assertEquals("MODIFIEDB", m._get("initArgs/defaults/b"));
   }
 
   public void testReqHandlerAPIs() throws Exception {
-    reqhandlertests(restTestHarness, null, null);
+    reqhandlertests(restTestHarness);
   }
 
   public static void runConfigCommand(RestTestHarness harness, String uri, 
String payload)
@@ -195,9 +187,7 @@ public class TestSolrConfigHandler extends RestTestBase {
         errorMessages.get(0).toString().contains(expectedErrorMessage));
   }
 
-  public static void reqhandlertests(
-      RestTestHarness writeHarness, String testServerBaseUrl, CloudSolrClient 
cloudSolrClient)
-      throws Exception {
+  public static void reqhandlertests(RestTestHarness writeHarness) throws 
Exception {
     String payload =
         "{\n"
             + "'create-requesthandler' : { 'name' : '/x', 'class': 
'org.apache.solr.handler.DumpRequestHandler' , 'startup' : 'lazy'}\n"
@@ -206,9 +196,7 @@ public class TestSolrConfigHandler extends RestTestBase {
 
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config/overlay",
-        cloudSolrClient,
         asList("overlay", "requestHandler", "/x", "startup"),
         "lazy",
         TIMEOUT_S);
@@ -222,9 +210,7 @@ public class TestSolrConfigHandler extends RestTestBase {
 
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config/overlay",
-        cloudSolrClient,
         asList("overlay", "requestHandler", "/x", "a"),
         "b",
         TIMEOUT_S);
@@ -240,27 +226,21 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config/overlay",
-        cloudSolrClient,
         asList("overlay", "requestHandler", "/dump", "defaults", "c"),
         "C",
         TIMEOUT_S);
 
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/x?getdefaults=true&json.nl=map",
-        cloudSolrClient,
         asList("getdefaults", "def_a"),
         "def A val",
         TIMEOUT_S);
 
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/x?param=multival&json.nl=map",
-        cloudSolrClient,
         asList("params", "multival"),
         asList("a", "b", "c"),
         TIMEOUT_S);
@@ -273,10 +253,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     while (TimeUnit.SECONDS.convert(System.nanoTime() - startTime, 
TimeUnit.NANOSECONDS)
         < maxTimeoutSeconds) {
       String uri = "/config/overlay";
-      Map<?, ?> m =
-          testServerBaseUrl == null
-              ? getRespMap(uri, writeHarness)
-              : TestSolrConfigHandlerConcurrent.getAsMap(testServerBaseUrl + 
uri, cloudSolrClient);
+      Map<?, ?> m = getRespMap(writeHarness, uri);
       if (null == Utils.getObjectByPath(m, true, asList("overlay", 
"requestHandler", "/x", "a"))) {
         success = true;
         break;
@@ -292,9 +269,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "queryConverter", "qc", "class"),
         "org.apache.solr.spelling.SpellingQueryConverter",
         TIMEOUT_S);
@@ -305,9 +280,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "queryConverter", "qc", "class"),
         "org.apache.solr.spelling.SuggestQueryConverter",
         TIMEOUT_S);
@@ -315,13 +288,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     payload = "{\n" + "'delete-queryconverter' : 'qc'" + "}";
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
-        writeHarness,
-        testServerBaseUrl,
-        "/config",
-        cloudSolrClient,
-        asList("config", "queryConverter", "qc"),
-        null,
-        TIMEOUT_S);
+        writeHarness, "/config", asList("config", "queryConverter", "qc"), 
null, TIMEOUT_S);
 
     payload =
         "{\n"
@@ -333,9 +300,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "listener[0]", "class"),
         "solr.QuerySenderListener",
         TIMEOUT_S);
@@ -350,23 +315,14 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "listener[0]", "class"),
         "org.apache.solr.core.QuerySenderListener",
         TIMEOUT_S);
 
     payload = "{\n" + "'delete-listener' : 'f7fb2d87bea44464af2401cf33f42b69'" 
+ "}";
     runConfigCommand(writeHarness, "/config", payload);
-    testForResponseElement(
-        writeHarness,
-        testServerBaseUrl,
-        "/config",
-        cloudSolrClient,
-        asList("config", "listener"),
-        null,
-        TIMEOUT_S);
+    testForResponseElement(writeHarness, "/config", asList("config", 
"listener"), null, TIMEOUT_S);
 
     payload =
         "{\n"
@@ -375,9 +331,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "searchComponent", "tc", "class"),
         "org.apache.solr.handler.component.TermsComponent",
         TIMEOUT_S);
@@ -388,9 +342,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "searchComponent", "tc", "class"),
         "org.apache.solr.handler.component.TermVectorComponent",
         TIMEOUT_S);
@@ -398,13 +350,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     payload = "{\n" + "'delete-searchcomponent' : 'tc'" + "}";
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
-        writeHarness,
-        testServerBaseUrl,
-        "/config",
-        cloudSolrClient,
-        asList("config", "searchComponent", "tc"),
-        null,
-        TIMEOUT_S);
+        writeHarness, "/config", asList("config", "searchComponent", "tc"), 
null, TIMEOUT_S);
     // <valueSourceParser name="countUsage"
     // class="org.apache.solr.core.CountUsageValueSourceParser"/>
     payload =
@@ -414,9 +360,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "valueSourceParser", "cu", "class"),
         "org.apache.solr.core.CountUsageValueSourceParser",
         TIMEOUT_S);
@@ -430,9 +374,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "valueSourceParser", "cu", "class"),
         "org.apache.solr.search.function.NvlValueSourceParser",
         TIMEOUT_S);
@@ -440,13 +382,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     payload = "{\n" + "'delete-valuesourceparser' : 'cu'" + "}";
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
-        writeHarness,
-        testServerBaseUrl,
-        "/config",
-        cloudSolrClient,
-        asList("config", "valueSourceParser", "cu"),
-        null,
-        TIMEOUT_S);
+        writeHarness, "/config", asList("config", "valueSourceParser", "cu"), 
null, TIMEOUT_S);
     //    <transformer name="mytrans2"
     // class="org.apache.solr.response.transform.ValueAugmenterFactory" >
     //    <int name="value">5</int>
@@ -458,9 +394,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "transformer", "mytrans", "class"),
         "org.apache.solr.response.transform.ValueAugmenterFactory",
         TIMEOUT_S);
@@ -472,9 +406,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config",
-        cloudSolrClient,
         asList("config", "transformer", "mytrans", "value"),
         "6",
         TIMEOUT_S);
@@ -487,13 +419,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     runConfigCommand(writeHarness, "/config", payload);
     Map<?, ?> map =
         testForResponseElement(
-            writeHarness,
-            testServerBaseUrl,
-            "/config",
-            cloudSolrClient,
-            asList("config", "transformer", "mytrans"),
-            null,
-            TIMEOUT_S);
+            writeHarness, "/config", asList("config", "transformer", 
"mytrans"), null, TIMEOUT_S);
 
     List<?> l = (List<?>) Utils.getObjectByPath(map, false, asList("config", 
"initParams"));
     assertNotNull("no object /config/initParams : " + map, l);
@@ -517,9 +443,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     map =
         testForResponseElement(
             writeHarness,
-            testServerBaseUrl,
             "/config",
-            cloudSolrClient,
             asList("config", "searchComponent", "myspellcheck", 
"spellchecker", "class"),
             "solr.DirectSolrSpellChecker",
             TIMEOUT_S);
@@ -538,14 +462,12 @@ public class TestSolrConfigHandler extends RestTestBase {
     map =
         testForResponseElement(
             writeHarness,
-            testServerBaseUrl,
             "/config",
-            cloudSolrClient,
             asList("config", "requestHandler", "/dump100", "class"),
             "org.apache.solr.handler.DumpRequestHandler",
             TIMEOUT_S);
 
-    map = getRespMap("/dump100?json.nl=arrmap&initArgs=true", writeHarness);
+    map = getRespMap(writeHarness, "/dump100?json.nl=arrmap&initArgs=true");
     List<?> initArgs = (List<?>) map.get("initArgs");
     assertNotNull(initArgs);
     assertTrue(initArgs.size() >= 2);
@@ -565,9 +487,7 @@ public class TestSolrConfigHandler extends RestTestBase {
 
     testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config/overlay",
-        cloudSolrClient,
         asList("overlay", "requestHandler", "/dump101", "startup"),
         "lazy",
         TIMEOUT_S);
@@ -581,9 +501,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     map =
         testForResponseElement(
             writeHarness,
-            testServerBaseUrl,
             "/config/overlay",
-            cloudSolrClient,
             asList("overlay", "cache", "lfuCacheDecayFalse", "class"),
             "solr.search.CaffeineCache",
             TIMEOUT_S);
@@ -592,7 +510,7 @@ public class TestSolrConfigHandler extends RestTestBase {
         getObjectByPath(map, true, List.of("overlay", "cache", "perSegFilter", 
"class")));
 
     map =
-        
getRespMap("/dump101?cacheNames=lfuCacheDecayFalse&cacheNames=perSegFilter", 
writeHarness);
+        getRespMap(writeHarness, 
"/dump101?cacheNames=lfuCacheDecayFalse&cacheNames=perSegFilter");
     assertEquals(
         "Actual output " + Utils.toJSONString(map),
         "org.apache.solr.search.CaffeineCache",
@@ -663,9 +581,7 @@ public class TestSolrConfigHandler extends RestTestBase {
   @SuppressWarnings({"unchecked", "rawtypes"})
   public static LinkedHashMapWriter testForResponseElement(
       RestTestHarness harness,
-      String testServerBaseUrl,
       String uri,
-      CloudSolrClient cloudSolrClient,
       List<String> jsonPath,
       Object expected,
       long maxTimeoutSeconds)
@@ -677,11 +593,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     TimeOut timeOut = new TimeOut(maxTimeoutSeconds, TimeUnit.SECONDS, 
TimeSource.NANO_TIME);
     while (!timeOut.hasTimedOut()) {
       try {
-        m =
-            testServerBaseUrl == null
-                ? getRespMap(uri, harness)
-                : TestSolrConfigHandlerConcurrent.getAsMap(
-                    testServerBaseUrl + uri, cloudSolrClient);
+        m = getRespMap(harness, uri);
       } catch (Exception e) {
         Thread.sleep(100);
         continue;
@@ -705,7 +617,7 @@ public class TestSolrConfigHandler extends RestTestBase {
     assertTrue(
         StrUtils.formatString(
             "Could not get expected value  ''{0}'' for path ''{1}'' full 
output: {2},  from server:  {3}",
-            expected, StrUtils.join(jsonPath, '/'), m.toString(), 
testServerBaseUrl),
+            expected, StrUtils.join(jsonPath, '/'), m.toString(), 
harness.getBaseURL()),
         success);
 
     return m;
@@ -724,22 +636,10 @@ public class TestSolrConfigHandler extends RestTestBase {
     TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/config/params",
-        null,
-        asList("response", "params", "x", "a"),
-        "A val",
-        TIMEOUT_S);
+        harness, "/config/params", asList("response", "params", "x", "a"), "A 
val", TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/config/params",
-        null,
-        asList("response", "params", "x", "b"),
-        "B val",
-        TIMEOUT_S);
+        harness, "/config/params", asList("response", "params", "x", "b"), "B 
val", TIMEOUT_S);
 
     payload =
         "{\n"
@@ -750,23 +650,15 @@ public class TestSolrConfigHandler extends RestTestBase {
 
     TestSolrConfigHandler.testForResponseElement(
         harness,
-        null,
         "/config/overlay",
-        null,
         asList("overlay", "requestHandler", "/d", "name"),
         "/d",
         TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness, null, "/d?useParams=x", null, asList("params", "a"), "A val", 
TIMEOUT_S);
+        harness, "/d?useParams=x", asList("params", "a"), "A val", TIMEOUT_S);
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/d?useParams=x&a=fomrequest",
-        null,
-        asList("params", "a"),
-        "fomrequest",
-        TIMEOUT_S);
+        harness, "/d?useParams=x&a=fomrequest", asList("params", "a"), 
"fomrequest", TIMEOUT_S);
 
     payload =
         "{\n"
@@ -777,15 +669,13 @@ public class TestSolrConfigHandler extends RestTestBase {
 
     TestSolrConfigHandler.testForResponseElement(
         harness,
-        null,
         "/config/overlay",
-        null,
         asList("overlay", "requestHandler", "/dump1", "name"),
         "/dump1",
         TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness, null, "/dump1", null, asList("params", "a"), "A val", 
TIMEOUT_S);
+        harness, "/dump1", asList("params", "a"), "A val", TIMEOUT_S);
 
     payload =
         " {\n"
@@ -799,31 +689,19 @@ public class TestSolrConfigHandler extends RestTestBase {
     TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/config/params",
-        null,
-        asList("response", "params", "y", "c"),
-        "CY val",
-        TIMEOUT_S);
+        harness, "/config/params", asList("response", "params", "y", "c"), "CY 
val", TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness, null, "/dump1?useParams=y", null, asList("params", "c"), "CY 
val", TIMEOUT_S);
+        harness, "/dump1?useParams=y", asList("params", "c"), "CY val", 
TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness, null, "/dump1?useParams=y", null, asList("params", "b"), "BY 
val", TIMEOUT_S);
+        harness, "/dump1?useParams=y", asList("params", "b"), "BY val", 
TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness, null, "/dump1?useParams=y", null, asList("params", "a"), "A 
val", TIMEOUT_S);
+        harness, "/dump1?useParams=y", asList("params", "a"), "A val", 
TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/dump1?useParams=y",
-        null,
-        asList("params", "d"),
-        asList("val 1", "val 2"),
-        TIMEOUT_S);
+        harness, "/dump1?useParams=y", asList("params", "d"), asList("val 1", 
"val 2"), TIMEOUT_S);
 
     payload =
         " {\n"
@@ -839,21 +717,13 @@ public class TestSolrConfigHandler extends RestTestBase {
 
     TestSolrConfigHandler.testForResponseElement(
         harness,
-        null,
         "/config/params",
-        null,
         asList("response", "params", "y", "c"),
         "CY val modified",
         TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/config/params",
-        null,
-        asList("response", "params", "y", "e"),
-        "EY val",
-        TIMEOUT_S);
+        harness, "/config/params", asList("response", "params", "y", "e"), "EY 
val", TIMEOUT_S);
 
     payload =
         " {\n"
@@ -866,32 +736,14 @@ public class TestSolrConfigHandler extends RestTestBase {
 
     TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload);
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/config/params",
-        null,
-        asList("response", "params", "y", "p"),
-        "P val",
-        TIMEOUT_S);
+        harness, "/config/params", asList("response", "params", "y", "p"), "P 
val", TIMEOUT_S);
 
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/config/params",
-        null,
-        asList("response", "params", "y", "c"),
-        null,
-        TIMEOUT_S);
+        harness, "/config/params", asList("response", "params", "y", "c"), 
null, TIMEOUT_S);
     payload = " {'delete' : 'y'}";
     TestSolrConfigHandler.runConfigCommand(harness, "/config/params", payload);
     TestSolrConfigHandler.testForResponseElement(
-        harness,
-        null,
-        "/config/params",
-        null,
-        asList("response", "params", "y", "p"),
-        null,
-        TIMEOUT_S);
+        harness, "/config/params", asList("response", "params", "y", "p"), 
null, TIMEOUT_S);
 
     payload =
         "{\n"
@@ -916,44 +768,35 @@ public class TestSolrConfigHandler extends RestTestBase {
     TestSolrConfigHandler.runConfigCommand(harness, "/config", payload);
     TestSolrConfigHandler.testForResponseElement(
         harness,
-        null,
         "/config/overlay",
-        null,
         asList("overlay", "requestHandler", "aRequestHandler", "class"),
         "org.apache.solr.handler.DumpRequestHandler",
         TIMEOUT_S);
-    RESTfulServerProvider oldProvider = restTestHarness.getServerProvider();
-    restTestHarness.setServerProvider(
-        () -> getBaseUrl() + "/____v2/cores/" + DEFAULT_TEST_CORENAME);
-
-    Map<?, ?> rsp =
-        TestSolrConfigHandler.testForResponseElement(
-            harness,
-            null,
-            
"/something/part1_Value/fixed/part2_Value?urlTemplateValues=part1&urlTemplateValues=part2",
-            null,
-            asList("urlTemplateValues"),
-            new ValidatingJsonMap.PredicateWithErrMsg<>() {
-              @Override
-              public String test(Object o) {
-                if (o instanceof Map<?, ?> m) {
-                  if ("part1_Value".equals(m.get("part1")) && 
"part2_Value".equals(m.get("part2")))
-                    return null;
-                }
-                return "no match";
-              }
-
-              @Override
-              public String toString() {
-                return "{part1:part1_Value, part2 : part2_Value]";
-              }
-            },
-            TIMEOUT_S);
-    restTestHarness.setServerProvider(oldProvider);
+
+    TestSolrConfigHandler.testForResponseElement(
+        harness.newWithUrl(getBaseUrl() + "/____v2/cores/" + 
DEFAULT_TEST_CORENAME),
+        
"/something/part1_Value/fixed/part2_Value?urlTemplateValues=part1&urlTemplateValues=part2",
+        asList("urlTemplateValues"),
+        new ValidatingJsonMap.PredicateWithErrMsg<>() {
+          @Override
+          public String test(Object o) {
+            if (o instanceof Map<?, ?> m) {
+              if ("part1_Value".equals(m.get("part1")) && 
"part2_Value".equals(m.get("part2")))
+                return null;
+            }
+            return "no match";
+          }
+
+          @Override
+          public String toString() {
+            return "{part1:part1_Value, part2 : part2_Value]";
+          }
+        },
+        TIMEOUT_S);
   }
 
   @SuppressWarnings({"rawtypes"})
-  public static LinkedHashMapWriter getRespMap(String path, RestTestHarness 
restHarness)
+  public static LinkedHashMapWriter getRespMap(RestTestHarness restHarness, 
String path)
       throws Exception {
     String response = restHarness.query(path);
     try {
@@ -966,8 +809,8 @@ public class TestSolrConfigHandler extends RestTestBase {
   }
 
   public void testCacheDisableSolrConfig() throws Exception {
-    RESTfulServerProvider oldProvider = restTestHarness.getServerProvider();
-    restTestHarness.setServerProvider(RestTestBase::getBaseUrl);
+    RestTestHarness oldRth = restTestHarness;
+    restTestHarness = restTestHarness.newWithUrl(getBaseUrl());
     String prometheusMetrics = 
restTestHarness.query("/admin/metrics?wt=prometheus");
     assertTrue(
         "fieldValueCache metrics should be present",
@@ -977,45 +820,45 @@ public class TestSolrConfigHandler extends RestTestBase {
         "documentCache metrics should be absent",
         prometheusMetrics.contains("name=\"documentCache\""));
 
-    restTestHarness.setServerProvider(oldProvider);
+    restTestHarness = oldRth;
   }
 
   public void testSetPropertyCacheSize() throws Exception {
-    RESTfulServerProvider oldProvider = restTestHarness.getServerProvider();
+    RestTestHarness oldRth = restTestHarness;
     // Changing cache size
     String payload = "{'set-property' : { 'query.documentCache.size': 399} }";
     runConfigCommand(restTestHarness, "/config", payload);
-    MapWriter overlay = getRespMap("/config/overlay", restTestHarness);
+    MapWriter overlay = getRespMap(restTestHarness, "/config/overlay");
     assertEquals("399", 
overlay._getStr("overlay/props/query/documentCache/size"));
     // Setting size only will not enable the cache
-    restTestHarness.setServerProvider(RestTestBase::getBaseUrl);
+    restTestHarness = restTestHarness.newWithUrl(getBaseUrl());
 
     String prometheusMetrics = 
restTestHarness.query("/admin/metrics?wt=prometheus");
     assertFalse(prometheusMetrics.contains("cache_name=\"documentCache\""));
 
-    restTestHarness.setServerProvider(oldProvider);
+    restTestHarness = oldRth;
   }
 
   public void testSetPropertyEnableAndDisableCache() throws Exception {
-    RESTfulServerProvider oldProvider = restTestHarness.getServerProvider();
+    RestTestHarness oldRth = restTestHarness;
     // Enabling Cache
     String payload = "{'set-property' : { 'query.documentCache.enabled': true} 
}";
     runConfigCommand(restTestHarness, "/config", payload);
-    restTestHarness.setServerProvider(RestTestBase::getBaseUrl);
+    restTestHarness = restTestHarness.newWithUrl(getBaseUrl());
 
     String prometheusMetrics = 
restTestHarness.query("/admin/metrics?wt=prometheus");
     assertTrue(prometheusMetrics.contains("name=\"documentCache\""));
 
     // Disabling Cache
     payload = "{ 'set-property' : { 'query.documentCache.enabled': false } }";
-    restTestHarness.setServerProvider(oldProvider);
+    restTestHarness = oldRth;
 
     runConfigCommand(restTestHarness, "/config", payload);
-    restTestHarness.setServerProvider(RestTestBase::getBaseUrl);
+    restTestHarness = restTestHarness.newWithUrl(getBaseUrl());
 
     prometheusMetrics = restTestHarness.query("/admin/metrics?wt=prometheus");
     assertFalse(prometheusMetrics.contains("name=\"documentCache\""));
 
-    restTestHarness.setServerProvider(oldProvider);
+    restTestHarness = oldRth;
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/TestReqParamsAPI.java 
b/solr/core/src/test/org/apache/solr/handler/TestReqParamsAPI.java
index ac3d82b1956..47eb83e7e9d 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestReqParamsAPI.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestReqParamsAPI.java
@@ -46,13 +46,9 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
   private void setupHarnesses() {
     for (final JettySolrRunner jettySolrRunner : 
cluster.getJettySolrRunners()) {
-      RestTestHarness harness =
-          new RestTestHarness(() -> jettySolrRunner.getBaseUrl().toString() + 
"/" + COLL_NAME);
-      if (random().nextBoolean()) {
-        harness.setServerProvider(
-            () -> jettySolrRunner.getBaseUrl().toString() + "/____v2/c/" + 
COLL_NAME);
-      }
-      restTestHarnesses.add(harness);
+      // Randomly pick v1 or v2 API
+      String path = random().nextBoolean() ? COLL_NAME : "____v2/c/" + 
COLL_NAME;
+      restTestHarnesses.add(jettySolrRunner.getRestClient(path));
     }
   }
 
@@ -70,14 +66,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
   @Test
   public void test() throws Exception {
-    try {
-      setupHarnesses();
-      testReqParams();
-    } finally {
-      for (RestTestHarness r : restTestHarnesses) {
-        r.close();
-      }
-    }
+    setupHarnesses();
+    testReqParams();
   }
 
   private void testReqParams() throws Exception {
@@ -120,60 +110,48 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
     Map<?, ?> result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "x", "a"),
             "A val",
             10);
     compareValues(result, "B val", asList("response", "params", "x", "b"));
 
     TestSolrConfigHandler.testForResponseElement(
-        null,
-        urls.get(random().nextInt(urls.size())),
+        writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
         "/config/overlay",
-        cloudClient,
         asList("overlay", "requestHandler", "/dump0", "name"),
         "/dump0",
         10);
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/dump0?useParams=x",
-            cloudClient,
             asList("params", "a"),
             "A val",
             5);
     compareValues(result, "", asList("params", RequestParams.USEPARAM));
 
     TestSolrConfigHandler.testForResponseElement(
-        null,
-        urls.get(random().nextInt(urls.size())),
+        writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
         "/dump0?useParams=x&a=fomrequest",
-        cloudClient,
         asList("params", "a"),
         "fomrequest",
         5);
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/overlay",
-            cloudClient,
             asList("overlay", "requestHandler", "/dump1", "name"),
             "/dump1",
             10);
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/dump1",
-            cloudClient,
             asList("params", "a"),
             "A val",
             5);
@@ -193,10 +171,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "y", "c"),
             "CY val",
             10);
@@ -205,10 +181,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/dump1?useParams=y",
-            cloudClient,
             asList("params", "c"),
             "CY val",
             5);
@@ -219,10 +193,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             
"/config/requestHandler?componentName=/dump1&expandParams=true&useParams=y&c=CC",
-            cloudClient,
             asList("config", "requestHandler", "/dump1", 
"_useParamsExpanded_", "x", "a"),
             "A val",
             5);
@@ -259,10 +231,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "y", "c"),
             "CY val modified",
             10);
@@ -280,10 +250,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
     TestSolrConfigHandler.runConfigCommand(writeHarness, "/config/params", 
payload);
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "y", "p"),
             "P val",
             10);
@@ -296,10 +264,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "x", "_appends_", "add"),
             "first",
             10);
@@ -307,10 +273,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/dump1?fixed=changeit&add=second",
-            cloudClient,
             asList("params", "fixed"),
             "f",
             5);
@@ -328,10 +292,8 @@ public class TestReqParamsAPI extends SolrCloudTestCase {
     payload = " {'delete' : 'y'}";
     TestSolrConfigHandler.runConfigCommand(writeHarness, "/config/params", 
payload);
     TestSolrConfigHandler.testForResponseElement(
-        null,
-        urls.get(random().nextInt(urls.size())),
+        writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
         "/config/params",
-        cloudClient,
         asList("response", "params", "y", "p"),
         null,
         10);
diff --git 
a/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerCloud.java 
b/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerCloud.java
index 87a31f226c1..3370a5c5f77 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerCloud.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerCloud.java
@@ -63,9 +63,7 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
 
     TestSolrConfigHandler.testForResponseElement(
         writeHarness,
-        testServerBaseUrl,
         "/config/overlay",
-        cloudClient,
         Arrays.asList("overlay", "requestHandler", "/admin/luke", "class"),
         "org.apache.solr.handler.DumpRequestHandler",
         TIMEOUT_S);
@@ -77,7 +75,7 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
   private void testReqHandlerAPIs() throws Exception {
     String testServerBaseUrl = getRandomServer(cloudClient, "collection1");
     RestTestHarness writeHarness = randomRestTestHarness();
-    TestSolrConfigHandler.reqhandlertests(writeHarness, testServerBaseUrl, 
cloudClient);
+    
TestSolrConfigHandler.reqhandlertests(writeHarness.newWithUrl(testServerBaseUrl));
   }
 
   public static String getRandomServer(CloudSolrClient cloudClient, String 
collName) {
@@ -111,10 +109,8 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
 
     Map<?, ?> result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "x", "a"),
             "A val",
             TIMEOUT_S);
@@ -128,30 +124,24 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
     TestSolrConfigHandler.runConfigCommand(writeHarness, "/config", payload);
 
     TestSolrConfigHandler.testForResponseElement(
-        null,
-        urls.get(random().nextInt(urls.size())),
+        writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
         "/config/overlay",
-        cloudClient,
         asList("overlay", "requestHandler", "/dump", "name"),
         "/dump",
         TIMEOUT_S);
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/dump?useParams=x",
-            cloudClient,
             asList("params", "a"),
             "A val",
             TIMEOUT_S);
     compareValues(result, "", asList("params", RequestParams.USEPARAM));
 
     TestSolrConfigHandler.testForResponseElement(
-        null,
-        urls.get(random().nextInt(urls.size())),
+        writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
         "/dump?useParams=x&a=fomrequest",
-        cloudClient,
         asList("params", "a"),
         "fomrequest",
         TIMEOUT_S);
@@ -165,20 +155,16 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/overlay",
-            cloudClient,
             asList("overlay", "requestHandler", "/dump1", "name"),
             "/dump1",
             TIMEOUT_S);
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/dump1",
-            cloudClient,
             asList("params", "a"),
             "A val",
             TIMEOUT_S);
@@ -198,10 +184,8 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "y", "c"),
             "CY val",
             TIMEOUT_S);
@@ -209,10 +193,8 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/dump?useParams=y",
-            cloudClient,
             asList("params", "c"),
             "CY val",
             TIMEOUT_S);
@@ -234,10 +216,8 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
 
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "y", "c"),
             "CY val modified",
             TIMEOUT_S);
@@ -255,10 +235,8 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
     TestSolrConfigHandler.runConfigCommand(writeHarness, "/config/params", 
payload);
     result =
         TestSolrConfigHandler.testForResponseElement(
-            null,
-            urls.get(random().nextInt(urls.size())),
+            writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
             "/config/params",
-            cloudClient,
             asList("response", "params", "y", "p"),
             "P val",
             TIMEOUT_S);
@@ -267,10 +245,8 @@ public class TestSolrConfigHandlerCloud extends 
AbstractFullDistribZkTestBase {
     payload = " {'delete' : 'y'}";
     TestSolrConfigHandler.runConfigCommand(writeHarness, "/config/params", 
payload);
     TestSolrConfigHandler.testForResponseElement(
-        null,
-        urls.get(random().nextInt(urls.size())),
+        writeHarness.newWithUrl(urls.get(random().nextInt(urls.size()))),
         "/config/params",
-        cloudClient,
         asList("response", "params", "y", "p"),
         null,
         TIMEOUT_S);
diff --git 
a/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerConcurrent.java
 
b/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerConcurrent.java
index 6730e580298..8a2a40aeacb 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerConcurrent.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/TestSolrConfigHandlerConcurrent.java
@@ -27,12 +27,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
-import org.apache.http.HttpEntity;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.util.EntityUtils;
 import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.client.solrj.apache.CloudLegacySolrClient;
-import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.cloud.AbstractFullDistribZkTestBase;
 import org.apache.solr.common.LinkedHashMapWriter;
 import org.apache.solr.common.MapWriter;
@@ -112,19 +107,15 @@ public class TestSolrConfigHandlerConcurrent extends 
AbstractFullDistribZkTestBa
       String val1;
       String val2;
       String val3;
-      try {
-        payload = payload.replace("CACHENAME", cacheName);
-        val1 = String.valueOf(10 * i + 1);
-        payload = payload.replace("CACHEVAL1", val1);
-        val2 = String.valueOf(10 * i + 2);
-        payload = payload.replace("CACHEVAL2", val2);
-        val3 = String.valueOf(10 * i + 3);
-        payload = payload.replace("CACHEVAL3", val3);
-
-        response = publisher.post("/config", SolrTestCaseJ4.json(payload));
-      } finally {
-        publisher.close();
-      }
+      payload = payload.replace("CACHENAME", cacheName);
+      val1 = String.valueOf(10 * i + 1);
+      payload = payload.replace("CACHEVAL1", val1);
+      val2 = String.valueOf(10 * i + 2);
+      payload = payload.replace("CACHEVAL2", val2);
+      val3 = String.valueOf(10 * i + 3);
+      payload = payload.replace("CACHEVAL3", val3);
+
+      response = publisher.post("/config", SolrTestCaseJ4.json(payload));
 
       Map<?, ?> map = (Map<?, ?>) Utils.fromJSONString(response);
       Object errors = map.get("errors");
@@ -142,6 +133,7 @@ public class TestSolrConfigHandlerConcurrent extends 
AbstractFullDistribZkTestBa
 
       // get another node
       String url = urls.get(urls.size() - 1);
+      RestTestHarness urlHarness = publisher.newWithUrl(url);
 
       long startTime = System.nanoTime();
       long maxTimeoutSeconds = 20;
@@ -149,7 +141,7 @@ public class TestSolrConfigHandlerConcurrent extends 
AbstractFullDistribZkTestBa
           < maxTimeoutSeconds) {
         Thread.sleep(100);
         errmessages.clear();
-        MapWriter respMap = getAsMap(url + "/config/overlay", cloudClient);
+        MapWriter respMap = getAsMap(urlHarness, "/config/overlay");
         MapWriter m = (MapWriter) respMap._get("overlay/props");
         if (m == null) {
           errmessages.add(
@@ -186,23 +178,14 @@ public class TestSolrConfigHandlerConcurrent extends 
AbstractFullDistribZkTestBa
   }
 
   @SuppressWarnings({"rawtypes"})
-  public static LinkedHashMapWriter getAsMap(String uri, CloudSolrClient 
cloudClient)
-      throws Exception {
-    HttpGet get = new HttpGet(uri);
-    HttpEntity entity = null;
+  public static LinkedHashMapWriter getAsMap(RestTestHarness harness, String 
uri) throws Exception {
+    String response = harness.query(uri);
     try {
-      entity = ((CloudLegacySolrClient) 
cloudClient).getHttpClient().execute(get).getEntity();
-      String response = EntityUtils.toString(entity, StandardCharsets.UTF_8);
-      try {
-        return (LinkedHashMapWriter)
-            Utils.MAPWRITEROBJBUILDER.apply(new JSONParser(new 
StringReader(response))).getVal();
-      } catch (JSONParser.ParseException e) {
-        log.error(response, e);
-        throw e;
-      }
-    } finally {
-      EntityUtils.consumeQuietly(entity);
-      get.releaseConnection();
+      return (LinkedHashMapWriter)
+          Utils.MAPWRITEROBJBUILDER.apply(new JSONParser(new 
StringReader(response))).getVal();
+    } catch (JSONParser.ParseException e) {
+      log.error(response, e);
+      throw e;
     }
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/rest/TestRestManager.java 
b/solr/core/src/test/org/apache/solr/rest/TestRestManager.java
index 3d7f6707bcd..698e388657c 100644
--- a/solr/core/src/test/org/apache/solr/rest/TestRestManager.java
+++ b/solr/core/src/test/org/apache/solr/rest/TestRestManager.java
@@ -120,6 +120,11 @@ public class TestRestManager extends SolrRestletTestBase {
     //    assertJQ("/config/managed", "/managedResources==[]");
   }
 
+  /** Helper method to verify HEAD request returns expected status code */
+  private void assertHead(String request, int expectedStatusCode) throws 
Exception {
+    assertEquals(expectedStatusCode, restTestHarness.head(request));
+  }
+
   @Test
   public void testReloadFromPersistentStorage() throws IOException {
     SolrResourceLoader loader = new SolrResourceLoader(Path.of("./"));
diff --git 
a/solr/core/src/test/org/apache/solr/rest/schema/TestBulkSchemaAPI.java 
b/solr/core/src/test/org/apache/solr/rest/schema/TestBulkSchemaAPI.java
index 14be7ddde90..def647fe426 100644
--- a/solr/core/src/test/org/apache/solr/rest/schema/TestBulkSchemaAPI.java
+++ b/solr/core/src/test/org/apache/solr/rest/schema/TestBulkSchemaAPI.java
@@ -70,10 +70,6 @@ public class TestBulkSchemaAPI extends RestTestBase {
   @After
   public void after() throws Exception {
     solrTestRule.reset();
-    if (restTestHarness != null) {
-      restTestHarness.close();
-    }
-    restTestHarness = null;
   }
 
   public void testMultipleAddFieldWithErrors() throws Exception {
diff --git 
a/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedStopFilterFactory.java
 
b/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedStopFilterFactory.java
index 9acb4da9ad0..2a0bcc385e3 100644
--- 
a/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedStopFilterFactory.java
+++ 
b/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedStopFilterFactory.java
@@ -51,11 +51,6 @@ public class TestManagedStopFilterFactory extends 
RestTestBase {
   @After
   public void after() throws Exception {
     solrTestRule.reset();
-
-    if (restTestHarness != null) {
-      restTestHarness.close();
-    }
-    restTestHarness = null;
   }
 
   /**
diff --git 
a/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymGraphFilterFactory.java
 
b/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymGraphFilterFactory.java
index 9fc2a2fcfd9..9bb95fc52a5 100644
--- 
a/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymGraphFilterFactory.java
+++ 
b/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymGraphFilterFactory.java
@@ -55,11 +55,6 @@ public class TestManagedSynonymGraphFilterFactory extends 
RestTestBase {
     if (null != tmpSolrHome) {
       PathUtils.deleteDirectory(tmpSolrHome);
     }
-
-    if (restTestHarness != null) {
-      restTestHarness.close();
-    }
-    restTestHarness = null;
   }
 
   @Test
diff --git 
a/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored2.java 
b/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored2.java
index 3ee29f9f9ed..0f34a9be823 100644
--- a/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored2.java
+++ b/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored2.java
@@ -45,11 +45,6 @@ public class TestUseDocValuesAsStored2 extends RestTestBase {
   @After
   public void after() throws Exception {
     solrTestRule.reset();
-
-    if (restTestHarness != null) {
-      restTestHarness.close();
-    }
-    restTestHarness = null;
   }
 
   public void testSchemaAPI() throws Exception {
diff --git 
a/solr/modules/language-models/src/test/org/apache/solr/languagemodels/TestLanguageModelBase.java
 
b/solr/modules/language-models/src/test/org/apache/solr/languagemodels/TestLanguageModelBase.java
index b5de672f6e8..a54e8e1875d 100644
--- 
a/solr/modules/language-models/src/test/org/apache/solr/languagemodels/TestLanguageModelBase.java
+++ 
b/solr/modules/language-models/src/test/org/apache/solr/languagemodels/TestLanguageModelBase.java
@@ -78,10 +78,7 @@ public class TestLanguageModelBase extends RestTestBase {
   }
 
   protected static void afterTest() throws Exception {
-    if (null != restTestHarness) {
-      restTestHarness.close();
-      restTestHarness = null;
-    }
+    restTestHarness = null;
     solrTestRule.reset();
     if (null != tmpSolrHome) {
       PathUtils.deleteDirectory(tmpSolrHome);
diff --git 
a/solr/modules/language-models/src/test/org/apache/solr/languagemodels/textvectorisation/store/rest/TestModelManagerPersistence.java
 
b/solr/modules/language-models/src/test/org/apache/solr/languagemodels/textvectorisation/store/rest/TestModelManagerPersistence.java
index ece13f4e1b7..92e8b68244e 100644
--- 
a/solr/modules/language-models/src/test/org/apache/solr/languagemodels/textvectorisation/store/rest/TestModelManagerPersistence.java
+++ 
b/solr/modules/language-models/src/test/org/apache/solr/languagemodels/textvectorisation/store/rest/TestModelManagerPersistence.java
@@ -91,8 +91,7 @@ public class TestModelManagerPersistence extends 
TestLanguageModelBase {
     assertJQ(ManagedTextToVectorModelStore.REST_END_POINT, 
"/models/[0]/params/logResponses==true");
 
     // check persistence after restart
-    getJetty().stop();
-    getJetty().start();
+    restartJetty();
     assertJQ(ManagedTextToVectorModelStore.REST_END_POINT, 
"/models/[0]/name=='" + modelName + "'");
     assertJQ(
         ManagedTextToVectorModelStore.REST_END_POINT,
@@ -118,8 +117,7 @@ public class TestModelManagerPersistence extends 
TestLanguageModelBase {
     assertJQ(ManagedTextToVectorModelStore.REST_END_POINT, "/models/==[]");
 
     // check persistence after restart
-    getJetty().stop();
-    getJetty().start();
+    restartJetty();
     assertJQ(ManagedTextToVectorModelStore.REST_END_POINT, "/models/==[]");
   }
 }
diff --git 
a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java 
b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java
index ed3a0c492c2..31a70b9652f 100644
--- a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java
+++ b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java
@@ -36,7 +36,6 @@ import org.apache.solr.ltr.feature.OriginalScoreFeature;
 import org.apache.solr.ltr.feature.SolrFeature;
 import org.apache.solr.ltr.feature.ValueFeature;
 import org.apache.solr.ltr.model.LinearModel;
-import org.apache.solr.util.RestTestHarness;
 import org.junit.AfterClass;
 import org.junit.Test;
 
@@ -63,7 +62,6 @@ public class TestLTROnSolrCloud extends TestRerankBase {
 
   @Override
   public void tearDown() throws Exception {
-    restTestHarness.close();
     restTestHarness = null;
     solrCluster.shutdown();
     super.tearDown();
@@ -332,8 +330,7 @@ public class TestLTROnSolrCloud extends TestRerankBase {
     for (JettySolrRunner solrRunner : solrCluster.getJettySolrRunners()) {
       if (!solrRunner.getCoreContainer().getCores().isEmpty()) {
         String coreName = 
solrRunner.getCoreContainer().getCores().iterator().next().getName();
-        restTestHarness =
-            new RestTestHarness(() -> solrRunner.getBaseUrl().toString() + "/" 
+ coreName);
+        restTestHarness = solrRunner.getRestClient(coreName);
         break;
       }
     }
diff --git a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestRerankBase.java 
b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestRerankBase.java
index a632080d8f3..acd3eeb3e59 100644
--- a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestRerankBase.java
+++ b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestRerankBase.java
@@ -190,10 +190,7 @@ public class TestRerankBase extends RestTestBase {
   }
 
   protected static void aftertest() throws Exception {
-    if (null != restTestHarness) {
-      restTestHarness.close();
-      restTestHarness = null;
-    }
+    restTestHarness = null;
     solrTestRule.reset();
     if (null != tmpSolrHome) {
       PathUtils.deleteDirectory(tmpSolrHome);
@@ -201,10 +198,6 @@ public class TestRerankBase extends RestTestBase {
     }
   }
 
-  public static void makeRestTestHarnessNull() {
-    restTestHarness = null;
-  }
-
   /** produces a model encoded in json * */
   public static String getModelInJson(
       String name, String type, String[] features, String fstore, String 
params) {
diff --git 
a/solr/modules/ltr/src/test/org/apache/solr/ltr/store/rest/TestModelManagerPersistence.java
 
b/solr/modules/ltr/src/test/org/apache/solr/ltr/store/rest/TestModelManagerPersistence.java
index 7b159e0e650..7fb717765c6 100644
--- 
a/solr/modules/ltr/src/test/org/apache/solr/ltr/store/rest/TestModelManagerPersistence.java
+++ 
b/solr/modules/ltr/src/test/org/apache/solr/ltr/store/rest/TestModelManagerPersistence.java
@@ -190,8 +190,7 @@ public class TestModelManagerPersistence extends 
TestRerankBase {
         "/features/[1]/name=='description'");
 
     // check persistence after restart
-    getJetty().stop();
-    getJetty().start();
+    restartJetty();
     assertJQ(ManagedModelStore.REST_END_POINT, "/models/[0]/name=='" + 
modelName + "'");
     assertJQ(
         ManagedFeatureStore.REST_END_POINT + "/" + 
FeatureStore.DEFAULT_FEATURE_STORE_NAME,
@@ -217,8 +216,7 @@ public class TestModelManagerPersistence extends 
TestRerankBase {
         "/error/msg=='Missing feature store: " + 
FeatureStore.DEFAULT_FEATURE_STORE_NAME + "'");
 
     // check persistence after restart
-    getJetty().stop();
-    getJetty().start();
+    restartJetty();
     assertJQ(ManagedModelStore.REST_END_POINT, "/models/==[]");
     assertJQ(
         ManagedFeatureStore.REST_END_POINT + "/" + 
FeatureStore.DEFAULT_FEATURE_STORE_NAME,
@@ -289,8 +287,7 @@ public class TestModelManagerPersistence extends 
TestRerankBase {
     doWrapperModelPersistenceChecks(modelName, FS_NAME, 
baseModelFile.getFileName().toString());
 
     // check persistence after restart
-    getJetty().stop();
-    getJetty().start();
+    restartJetty();
     doWrapperModelPersistenceChecks(modelName, FS_NAME, 
baseModelFile.getFileName().toString());
 
     // delete test settings
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/URLUtil.java 
b/solr/solrj/src/java/org/apache/solr/common/util/URLUtil.java
index 2cc577d5abe..e4e97e355ac 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/URLUtil.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/URLUtil.java
@@ -23,6 +23,7 @@ import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.apache.solr.common.params.SolrParams;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -157,4 +158,109 @@ public class URLUtil {
     URL url = new URI(solrUrl).toURL();
     return url.getAuthority() + url.getPath().replace('/', '_');
   }
+
+  /**
+   * Constructs a properly encoded URI by combining a base URI with a request 
path. This ensures
+   * special characters (like umlauts) are properly percent-encoded.
+   *
+   * @param baseUri the base URI (e.g., URI for "http://localhost:8983/solr";)
+   * @param path the path to append (e.g., "/config/overlay" or "admin/cores")
+   * @return a properly encoded URI
+   * @throws IllegalArgumentException if the baseUri or path are invalid
+   */
+  public static URI buildURI(URI baseUri, String path) {
+    return buildURI(baseUri, path, null);
+  }
+
+  /**
+   * Constructs a properly encoded URI by combining a base URI with a request 
path and optional
+   * query parameters. This ensures special characters are properly 
percent-encoded.
+   *
+   * @param baseUri the base URI (e.g., URI for "http://localhost:8983/solr";)
+   * @param path the path to append (e.g., "/config/overlay" or 
"admin/cores"). May include a query
+   *     string (e.g., "/select?q=test")
+   * @param queryParams optional query parameters to append (may be null). If 
the path already
+   *     contains a query string, these params will be added to it.
+   * @return a properly encoded URI
+   * @throws IllegalArgumentException if the baseUri or path are invalid
+   */
+  public static URI buildURI(URI baseUri, String path, SolrParams queryParams) 
{
+    if (baseUri == null) {
+      throw new IllegalArgumentException("baseUri cannot be null");
+    }
+    if (path == null || path.isEmpty()) {
+      throw new IllegalArgumentException("path cannot be null or empty");
+    }
+
+    try {
+      // The path may contain a query string (e.g., "/path?param=value")
+      // Split on the first '?' to separate path from query
+      String pathOnly;
+      String queryFromPath = null;
+      int queryIndex = path.indexOf('?');
+      if (queryIndex != -1) {
+        pathOnly = path.substring(0, queryIndex);
+        queryFromPath = path.substring(queryIndex + 1);
+      } else {
+        pathOnly = path;
+      }
+
+      // Normalize path: remove leading slash if present
+      String normalizedPath = pathOnly.startsWith("/") ? pathOnly.substring(1) 
: pathOnly;
+
+      // Construct the full path by combining base path with the additional 
path
+      String basePath = baseUri.getPath();
+      if (basePath == null) {
+        basePath = "";
+      }
+      // Ensure base path ends with '/' for proper concatenation
+      if (!basePath.isEmpty() && !basePath.endsWith("/")) {
+        basePath += "/";
+      }
+      String fullPath = basePath + normalizedPath;
+
+      // Combine query strings before building final URI
+      // Both queryFromPath and queryParams.toQueryString() are already 
percent-encoded
+      String combinedEncodedQuery = null;
+      if (queryFromPath != null || queryParams != null) {
+        StringBuilder queryBuilder = new StringBuilder();
+        if (queryFromPath != null) {
+          queryBuilder.append(queryFromPath);
+        }
+        if (queryParams != null) {
+          String encodedParams =
+              queryParams.toQueryString(); // returns 
"?name1=value1&name2=value2"
+          if (encodedParams.length() > 1) { // More than just "?"
+            if (!queryBuilder.isEmpty()) {
+              queryBuilder.append('&');
+            }
+            queryBuilder.append(encodedParams.substring(1)); // Skip leading 
'?'
+          }
+        }
+        if (!queryBuilder.isEmpty()) {
+          combinedEncodedQuery = queryBuilder.toString();
+        }
+      }
+
+      // Build URI with path only (no query)
+      URI uri =
+          new URI(
+              baseUri.getScheme(),
+              baseUri.getUserInfo(),
+              baseUri.getHost(),
+              baseUri.getPort(),
+              fullPath,
+              null, // no query - we'll add it to the string
+              baseUri.getFragment());
+
+      // If we have a combined query, append it to the URI string
+      if (combinedEncodedQuery != null) {
+        return URI.create(uri.toASCIIString() + '?' + combinedEncodedQuery);
+      }
+
+      return uri;
+    } catch (URISyntaxException e) {
+      throw new IllegalArgumentException("Failed to construct URI", e);
+    }
+  }
 }
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java 
b/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java
index 6bb6ba96ab6..72f6640a3a2 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java
@@ -107,10 +107,6 @@ public class SchemaTest extends RestTestBase {
   @After
   public void cleanup() throws Exception {
     solrTestRule.reset();
-    if (restTestHarness != null) {
-      restTestHarness.close();
-    }
-    restTestHarness = null;
   }
 
   @Test
diff --git a/solr/solrj/src/test/org/apache/solr/common/util/URLUtilTest.java 
b/solr/solrj/src/test/org/apache/solr/common/util/URLUtilTest.java
index 1ddb06cc8cc..946bddc8980 100644
--- a/solr/solrj/src/test/org/apache/solr/common/util/URLUtilTest.java
+++ b/solr/solrj/src/test/org/apache/solr/common/util/URLUtilTest.java
@@ -20,8 +20,10 @@ import static 
org.apache.solr.common.util.URLUtil.getBaseUrlForNodeName;
 import static org.apache.solr.common.util.URLUtil.getNodeNameForBaseUrl;
 
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URISyntaxException;
 import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.params.ModifiableSolrParams;
 import org.junit.Test;
 
 public class URLUtilTest extends SolrTestCase {
@@ -125,4 +127,158 @@ public class URLUtilTest extends SolrTestCase {
         "https://app-node-1:8983/api";,
         getBaseUrlForNodeName("app-node-1:8983_solr", "https", true));
   }
+
+  @Test
+  public void testBuildURIBasic() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    // Test with and without trailing/leading slashes
+    URI uri1 = URLUtil.buildURI(baseUri, "/config/overlay");
+    assertEquals("http://localhost:8983/solr/config/overlay";, uri1.toString());
+
+    URI uri2 = URLUtil.buildURI(URI.create("http://localhost:8983/solr/";), 
"config/overlay");
+    assertEquals("http://localhost:8983/solr/config/overlay";, uri2.toString());
+
+    URI uri3 = URLUtil.buildURI(baseUri, "admin/cores");
+    assertEquals("http://localhost:8983/solr/admin/cores";, uri3.toString());
+
+    URI uri4 = URLUtil.buildURI(URI.create("http://localhost:8983/solr/";), 
"/admin/cores");
+    assertEquals("http://localhost:8983/solr/admin/cores";, uri4.toString());
+  }
+
+  @Test
+  public void testBuildURIWithSpecialCharactersInPath() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    // Test with umlaut in path
+    URI uri1 = URLUtil.buildURI(baseUri, "config/überlay");
+    assertEquals("http://localhost:8983/solr/config/%C3%BCberlay";, 
uri1.toASCIIString());
+
+    // Test with multiple special characters
+    URI uri2 = URLUtil.buildURI(baseUri, "cöllection/döc");
+    assertEquals("http://localhost:8983/solr/c%C3%B6llection/d%C3%B6c";, 
uri2.toASCIIString());
+
+    // Test with spaces (should be encoded)
+    URI uri3 = URLUtil.buildURI(baseUri, "admin/my core");
+    assertEquals("http://localhost:8983/solr/admin/my%20core";, 
uri3.toASCIIString());
+  }
+
+  @Test
+  public void testBuildURIWithQueryParams() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("action", "STATUS");
+    params.add("wt", "json");
+
+    URI uri = URLUtil.buildURI(baseUri, "admin/cores", params);
+    String uriString = uri.toString();
+    
assertTrue(uriString.startsWith("http://localhost:8983/solr/admin/cores?";));
+    assertTrue(uriString.contains("action=STATUS"));
+    assertTrue(uriString.contains("wt=json"));
+    assertFalse(uriString.contains("??")); // Should NOT have double '?'
+
+    // Verify exactly one '?' character
+    int questionMarkCount = 0;
+    for (int i = 0; i < uriString.length(); i++) {
+      if (uriString.charAt(i) == '?') questionMarkCount++;
+    }
+    assertEquals("Should have exactly one '?' in the URI", 1, 
questionMarkCount);
+  }
+
+  @Test
+  public void testBuildURIWithSpecialCharactersInQueryParams() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("q", "naïve");
+    params.add("fq", "field:übung");
+
+    URI uri = URLUtil.buildURI(baseUri, "select", params);
+    String uriString = uri.toASCIIString();
+    assertTrue(uriString.startsWith("http://localhost:8983/solr/select?";));
+    // Query params should be percent-encoded by SolrParams.toQueryString()
+    assertTrue(uriString.contains("na%C3%AFve"));
+    assertTrue(uriString.contains("%C3%BCbung"));
+  }
+
+  @Test
+  public void testBuildURIWithPathAndQueryParamsWithSpecialChars() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("name", "Müller");
+    params.add("city", "Zürich");
+
+    URI uri = URLUtil.buildURI(baseUri, "cöre/select", params);
+    String uriString = uri.toASCIIString();
+    
assertTrue(uriString.startsWith("http://localhost:8983/solr/c%C3%B6re/select?";));
+    // Check that both path and query params are encoded
+    assertTrue(uriString.contains("M%C3%BCller"));
+    assertTrue(uriString.contains("Z%C3%BCrich"));
+  }
+
+  @Test
+  public void testBuildURIWithSpecialCharsInParamValues() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    // Test ampersand and equals sign as literal values in parameters
+    params.add("q", "foo=bar&baz");
+    params.add("fq", "field:a=b");
+
+    URI uri = URLUtil.buildURI(baseUri, "select", params);
+    String uriString = uri.toASCIIString();
+
+    // These characters should be percent-encoded in values
+    // URLEncoder also encodes ':' to %3A
+    assertTrue(
+        "Expected encoded = and & in value, got: " + uriString,
+        uriString.contains("foo%3Dbar%26baz"));
+    assertTrue(
+        "Expected encoded = and : in value, got: " + uriString,
+        uriString.contains("field%3Aa%3Db"));
+  }
+
+  @Test
+  public void testBuildURIValidation() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    // Test null baseUri
+    assertThrows(IllegalArgumentException.class, () -> URLUtil.buildURI(null, 
"path"));
+
+    // Test null/empty path
+    assertThrows(IllegalArgumentException.class, () -> 
URLUtil.buildURI(baseUri, null));
+    assertThrows(IllegalArgumentException.class, () -> 
URLUtil.buildURI(baseUri, ""));
+  }
+
+  @Test
+  public void testBuildURIWithQueryStringInPath() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    // Test with query string in path parameter
+    URI uri1 = URLUtil.buildURI(baseUri, "/select?q=test");
+    assertEquals("http://localhost:8983/solr/select?q=test";, uri1.toString());
+
+    // Test with query string with special characters
+    URI uri2 = URLUtil.buildURI(baseUri, "/select?q=naïve");
+    String uriString = uri2.toASCIIString();
+    assertTrue(uriString.startsWith("http://localhost:8983/solr/select?";));
+    assertTrue(uriString.contains("na%C3%AFve"));
+  }
+
+  @Test
+  public void testBuildURIWithQueryStringInPathAndParams() {
+    URI baseUri = URI.create("http://localhost:8983/solr";);
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("wt", "json");
+
+    // Test combining query string from path with additional params
+    URI uri = URLUtil.buildURI(baseUri, "/select?q=test", params);
+    String uriString = uri.toString();
+    assertTrue(uriString.startsWith("http://localhost:8983/solr/select?";));
+    assertTrue(uriString.contains("q=test"));
+    assertTrue(uriString.contains("wt=json"));
+    assertTrue(uriString.contains("&")); // Should have & separator
+    assertFalse(uriString.contains("??")); // Should NOT have double '?'
+
+    // Verify exactly one '?' character
+    int questionMarkCount = 0;
+    for (int i = 0; i < uriString.length(); i++) {
+      if (uriString.charAt(i) == '?') questionMarkCount++;
+    }
+    assertEquals("Should have exactly one '?' in the URI", 1, 
questionMarkCount);
+  }
 }
diff --git 
a/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java
 
b/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java
index 0bb4f08edb2..7f452aa4e61 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java
@@ -3107,17 +3107,13 @@ public abstract class AbstractFullDistribZkTestBase 
extends BaseDistributedSearc
 
   protected void setupRestTestHarnesses() {
     for (final JettySolrRunner jetty : jettys) {
-      RestTestHarness harness =
-          new RestTestHarness(
-              () -> jetty.getBaseUrl().toString() + "/" + 
DEFAULT_TEST_COLLECTION_NAME);
-      restTestHarnesses.add(harness);
+      restTestHarnesses.add(jetty.getRestClient(DEFAULT_TEST_COLLECTION_NAME));
     }
   }
 
   protected void closeRestTestHarnesses() throws IOException {
-    for (RestTestHarness h : restTestHarnesses) {
-      h.close();
-    }
+    // note: prior to Solr 10.1, there was actual closing to do.
+    restTestHarnesses.clear();
   }
 
   protected RestTestHarness randomRestTestHarness() {
diff --git 
a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java 
b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
index 1c6405675ee..9428b6878c4 100644
--- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
+++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
@@ -69,6 +69,7 @@ import org.apache.solr.servlet.RateLimitFilter;
 import org.apache.solr.servlet.RequiredSolrRequestFilter;
 import org.apache.solr.servlet.SolrServlet;
 import org.apache.solr.servlet.TracingFilter;
+import org.apache.solr.util.RestTestHarness;
 import org.apache.solr.util.SocketProxy;
 import org.apache.solr.util.TimeOut;
 import org.apache.solr.util.configuration.SSLConfigurationsFactory;
@@ -808,19 +809,27 @@ public class JettySolrRunner implements SolrBackend {
     this.proxyPort = proxyPort;
   }
 
+  private URI getBaseUri(int jettyPort, String path) {
+    try {
+      return new URI(protocol, null, host, jettyPort, path, null, null);
+    } catch (URISyntaxException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   /** Returns a base URL like {@code http://localhost:8983/solr} */
   public URL getBaseUrl() {
     try {
-      return new URI(protocol, null, host, jettyPort, "/solr", null, 
null).toURL();
-    } catch (URISyntaxException | MalformedURLException e) {
+      return getBaseUri(jettyPort, "/solr").toURL();
+    } catch (MalformedURLException e) {
       throw new RuntimeException(e);
     }
   }
 
   public URL getBaseURLV2() {
     try {
-      return new URI(protocol, null, host, jettyPort, "/api", null, 
null).toURL();
-    } catch (MalformedURLException | URISyntaxException e) {
+      return getBaseUri(jettyPort, "/api").toURL();
+    } catch (MalformedURLException e) {
       throw new RuntimeException(e);
     }
   }
@@ -831,8 +840,8 @@ public class JettySolrRunner implements SolrBackend {
    */
   public URL getProxyBaseUrl() {
     try {
-      return new URI(protocol, null, host, getLocalPort(), "/solr", null, 
null).toURL();
-    } catch (MalformedURLException | URISyntaxException e) {
+      return getBaseUri(getLocalPort(), "/solr").toURL();
+    } catch (MalformedURLException e) {
       throw new RuntimeException(e);
     }
   }
@@ -907,6 +916,18 @@ public class JettySolrRunner implements SolrBackend {
     return proxy;
   }
 
+  /**
+   * Creates a REST client useful for HTTP operations. It closes when this 
{@link JettySolrRunner}
+   * is stopped.
+   */
+  public RestTestHarness getRestClient(String collection) {
+    String path = "/solr";
+    if (collection != null) {
+      path += "/" + collection;
+    }
+    return new RestTestHarness(getSolrClient().getHttpClient(), 
getBaseUri(jettyPort, path));
+  }
+
   // ---- SolrBackend implementation ----
 
   @Override
diff --git 
a/solr/test-framework/src/java/org/apache/solr/handler/BackupRestoreUtils.java 
b/solr/test-framework/src/java/org/apache/solr/handler/BackupRestoreUtils.java
index bd610200895..7bd4fe12ee7 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/handler/BackupRestoreUtils.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/handler/BackupRestoreUtils.java
@@ -21,20 +21,21 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
-import org.apache.http.client.utils.URIBuilder;
 import org.apache.lucene.tests.util.TestUtil;
 import org.apache.solr.SolrTestCase;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.URLUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -77,42 +78,25 @@ public class BackupRestoreUtils extends SolrTestCase {
 
   public static void runCoreAdminCommand(
       String baseUrl, String coreName, String action, Map<String, String> 
params)
-      throws IOException, URISyntaxException {
-    final URI uri = new URI(baseUrl);
-    final var oldPath = uri.getPath() != null ? uri.getPath().substring(1) : 
"";
-    final var newPath = "admin/cores";
-    final var finalPath = oldPath.isEmpty() ? newPath : oldPath + "/" + 
newPath;
-
-    final URIBuilder builder =
-        new URIBuilder(uri)
-            .setPath(finalPath)
-            .addParameter("action", action)
-            .addParameter("core", coreName);
-
-    // Add additional parameters using loop
-    for (Map.Entry<String, String> entry : params.entrySet()) {
-      builder.addParameter(entry.getKey(), entry.getValue());
-    }
-
-    executeHttpRequest(builder.build());
+      throws IOException {
+    executeHttpRequest(
+        URLUtil.buildURI(
+            URI.create(baseUrl),
+            "admin/cores",
+            SolrParams.wrapDefaults(
+                new MapSolrParams(params),
+                new MapSolrParams(Map.of("action", action, "core", 
coreName)))));
   }
 
   public static void runReplicationHandlerCommand(
       String baseUrl, String coreName, String action, String repoName, String 
backupName)
-      throws IOException, URISyntaxException {
-    final URI uri = new URI(baseUrl);
-    final var oldPath = uri.getPath() != null ? uri.getPath().substring(1) : 
"";
-    final var newPath = coreName + ReplicationHandler.PATH;
-    final var finalPath = oldPath.isEmpty() ? newPath : oldPath + "/" + 
newPath;
-
-    final URI finalURI =
-        new URIBuilder(uri)
-            .setPath(finalPath)
-            .addParameter("command", action)
-            .addParameter("repository", repoName)
-            .addParameter("name", backupName)
-            .build();
-    executeHttpRequest(finalURI);
+      throws IOException {
+    executeHttpRequest(
+        URLUtil.buildURI(
+            URI.create(baseUrl),
+            coreName + ReplicationHandler.PATH,
+            new MapSolrParams(
+                Map.of("command", action, "repository", repoName, "name", 
backupName))));
   }
 
   private static void executeHttpRequest(URI uri) throws IOException {
diff --git 
a/solr/test-framework/src/java/org/apache/solr/util/RestTestBase.java 
b/solr/test-framework/src/java/org/apache/solr/util/RestTestBase.java
index a8b18812e32..83b2d302c0c 100644
--- a/solr/test-framework/src/java/org/apache/solr/util/RestTestBase.java
+++ b/solr/test-framework/src/java/org/apache/solr/util/RestTestBase.java
@@ -22,11 +22,9 @@ import java.nio.file.Path;
 import java.util.Map;
 import java.util.Properties;
 import javax.xml.xpath.XPathExpressionException;
-import org.apache.http.client.HttpClient;
 import org.apache.solr.JSONTestUtil;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.apache.HttpSolrClient;
 import org.apache.solr.common.params.MultiMapSolrParams;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.embedded.JettyConfig;
@@ -47,12 +45,8 @@ public abstract class RestTestBase extends SolrTestCaseJ4 {
   protected static RestTestHarness restTestHarness;
 
   @AfterClass
-  public static void cleanUpHarness() throws IOException {
-    RestTestHarness localHarness = restTestHarness;
-    if (localHarness != null) {
-      localHarness.close();
-      restTestHarness = null;
-    }
+  public static void cleanUpHarness() {
+    restTestHarness = null;
   }
 
   /**
@@ -78,13 +72,23 @@ public abstract class RestTestBase extends SolrTestCaseJ4 {
         .withSchemaFile(schemaFile)
         .create();
 
-    restTestHarness = new RestTestHarness(RestTestBase::getCoreUrl);
+    restTestHarness = getJetty().getRestClient(DEFAULT_TEST_CORENAME);
   }
 
   protected static JettySolrRunner getJetty() {
     return solrTestRule.getJetty();
   }
 
+  /**
+   * Restarts Jetty and recreates the RestTestHarness with a new HttpClient. 
Use this instead of
+   * calling getJetty().stop()/start() directly.
+   */
+  protected static void restartJetty() throws Exception {
+    getJetty().stop();
+    getJetty().start();
+    restTestHarness = getJetty().getRestClient(DEFAULT_TEST_CORENAME);
+  }
+
   /** URL to Solr */
   protected static String getBaseUrl() {
     return solrTestRule.getBaseUrl();
@@ -99,11 +103,6 @@ public abstract class RestTestBase extends SolrTestCaseJ4 {
     return solrTestRule.getSolrClient();
   }
 
-  protected static HttpClient getHttpClient() {
-    HttpSolrClient client = (HttpSolrClient) getSolrClient();
-    return client.getHttpClient();
-  }
-
   /** Validates an update XML String is successful */
   public static void assertU(String update) {
     assertU(null, update);
@@ -215,12 +214,6 @@ public abstract class RestTestBase extends SolrTestCaseJ4 {
     }
   }
 
-  public static void assertHead(String request, int expectedStatusCode) throws 
IOException {
-    String response = restTestHarness.head(request);
-    assertTrue(response.contains("HTTP/1.1 " + expectedStatusCode));
-    assertTrue(response.contains("Content-Length: 0"));
-  }
-
   /**
    * Makes a query request and returns the JSON string response
    *
diff --git 
a/solr/test-framework/src/java/org/apache/solr/util/RestTestHarness.java 
b/solr/test-framework/src/java/org/apache/solr/util/RestTestHarness.java
index 18c1f3355ce..910d5d76a24 100644
--- a/solr/test-framework/src/java/org/apache/solr/util/RestTestHarness.java
+++ b/solr/test-framework/src/java/org/apache/solr/util/RestTestHarness.java
@@ -16,50 +16,74 @@
  */
 package org.apache.solr.util;
 
-import java.io.Closeable;
 import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 import javax.xml.xpath.XPathConstants;
 import javax.xml.xpath.XPathExpressionException;
-import org.apache.http.HttpEntity;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpHead;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.util.EntityUtils;
-import org.apache.solr.client.solrj.apache.HttpClientUtil;
-import org.apache.solr.common.params.ModifiableSolrParams;
-
-/** Facilitates testing Solr's REST API via a provided embedded Jetty */
-public class RestTestHarness extends BaseTestHarness implements Closeable {
-  private RESTfulServerProvider serverProvider;
-  private CloseableHttpClient httpClient = HttpClientUtil.createClient(new 
ModifiableSolrParams());
-
-  public RestTestHarness(RESTfulServerProvider serverProvider) {
-    this.serverProvider = serverProvider;
+import org.apache.solr.common.util.URLUtil;
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.Request;
+import org.eclipse.jetty.client.StringRequestContent;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Facilitates testing Solr's REST API via Jetty HttpClient. Immutable and 
cheap to construct. */
+public class RestTestHarness extends BaseTestHarness {
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final HttpClient httpClient;
+  private final URI baseUri;
+
+  /**
+   * Creates a RestTestHarness for the given base URL using the provided 
HttpClient.
+   *
+   * @param httpClient The HttpClient to use for requests (not closed by this 
harness)
+   * @param baseUri The base URI (e.g., 
"http://localhost:8983/solr/collection1";)
+   */
+  public RestTestHarness(HttpClient httpClient, URI baseUri) {
+    if (httpClient == null) {
+      throw new IllegalArgumentException("httpClient cannot be null");
+    }
+    if (baseUri == null || !baseUri.isAbsolute()) {
+      throw new IllegalArgumentException("baseUrl cannot be null or 
non-absolute");
+    }
+    this.httpClient = httpClient;
+    this.baseUri = baseUri;
   }
 
-  public String getBaseURL() {
-    return serverProvider.getBaseURL();
+  /** Creates a new instance for the specified URL, sharing the underlying 
HTTP client. */
+  public RestTestHarness newWithUrl(String baseUrl) {
+    return new RestTestHarness(httpClient, URI.create(baseUrl));
   }
 
-  public void setServerProvider(RESTfulServerProvider serverProvider) {
-    this.serverProvider = serverProvider;
+  @Override
+  public String toString() {
+    return getBaseURL();
   }
 
-  public RESTfulServerProvider getServerProvider() {
-    return this.serverProvider;
+  public String getBaseURL() {
+    return getBaseURI().toASCIIString();
+  }
+
+  public URI getBaseURI() {
+    return baseUri;
   }
 
   public String getAdminURL() {
     return getBaseURL().replace("/collection1", "");
   }
 
+  private URI getAdminURI() {
+    return URI.create(getAdminURL());
+  }
+
   /**
    * Processes a "query" using a URL path (with no context path) + optional 
query params, e.g.
    * "/schema/fields?indent=off"
@@ -69,25 +93,29 @@ public class RestTestHarness extends BaseTestHarness 
implements Closeable {
    * @exception IOException any exception in the response.
    */
   public String query(String request) throws IOException {
-    return getResponse(new HttpGet(getBaseURL() + request));
+    return sendAndReturnString(
+        getHttpClient().newRequest(buildUri(request)).method(HttpMethod.GET));
   }
 
   public String adminQuery(String request) throws IOException {
-    return getResponse(new HttpGet(getAdminURL() + request));
+    return sendAndReturnString(
+        getHttpClient()
+            .newRequest(URLUtil.buildURI(getAdminURI(), request))
+            .method(HttpMethod.GET));
   }
 
   /**
    * Processes a HEAD request using a URL path (with no context path) + 
optional query params and
-   * returns the response content.
+   * returns the status code.
    *
    * @param request The URL path and optional query params
-   * @return The response to the HEAD request
+   * @return The HTTP status code
    */
-  public String head(String request) throws IOException {
-    HttpHead httpHead = new HttpHead(getBaseURL() + request);
-    return httpClient
-        .execute(httpHead, HttpClientUtil.createNewHttpClientRequestContext())
-        .toString();
+  public int head(String request) throws IOException {
+    ContentResponse rsp =
+        
send(getHttpClient().newRequest(buildUri(request)).method(HttpMethod.HEAD));
+    assert rsp.getHeaders().getLongField(HttpHeader.CONTENT_LENGTH) <= 0 : 
"Expected no content";
+    return rsp.getStatus();
   }
 
   /**
@@ -95,15 +123,15 @@ public class RestTestHarness extends BaseTestHarness 
implements Closeable {
    * "/schema/fields/newfield", PUTs the given content, and returns the 
response content.
    *
    * @param request The URL path and optional query params
-   * @param content The content to include with the PUT request
+   * @param json The content to include with the PUT request
    * @return The response to the PUT request
    */
-  public String put(String request, String content) throws IOException {
-    HttpPut httpPut = new HttpPut(getBaseURL() + request);
-    httpPut.setEntity(
-        new StringEntity(content, ContentType.create("application/json", 
StandardCharsets.UTF_8)));
-
-    return getResponse(httpPut);
+  public String put(String request, String json) throws IOException {
+    return sendAndReturnString(
+        getHttpClient()
+            .newRequest(buildUri(request))
+            .method(HttpMethod.PUT)
+            .body(new StringRequestContent("application/json", json, 
StandardCharsets.UTF_8)));
   }
 
   /**
@@ -114,8 +142,8 @@ public class RestTestHarness extends BaseTestHarness 
implements Closeable {
    * @return The response to the DELETE request
    */
   public String delete(String request) throws IOException {
-    HttpDelete httpDelete = new HttpDelete(getBaseURL() + request);
-    return getResponse(httpDelete);
+    return sendAndReturnString(
+        
getHttpClient().newRequest(buildUri(request)).method(HttpMethod.DELETE));
   }
 
   /**
@@ -123,30 +151,20 @@ public class RestTestHarness extends BaseTestHarness 
implements Closeable {
    * "/schema/fields/newfield", PUTs the given content, and returns the 
response content.
    *
    * @param request The URL path and optional query params
-   * @param content The content to include with the POST request
+   * @param json The content to include with the POST request
    * @return The response to the POST request
    */
-  public String post(String request, String content) throws IOException {
-    HttpPost httpPost = new HttpPost(getBaseURL() + request);
-    httpPost.setEntity(
-        new StringEntity(content, ContentType.create("application/json", 
StandardCharsets.UTF_8)));
-
-    return getResponse(httpPost);
+  public String post(String request, String json) throws IOException {
+    return sendAndReturnString(
+        getHttpClient()
+            .newRequest(buildUri(request))
+            .method(HttpMethod.POST)
+            .body(new StringRequestContent("application/json", json, 
StandardCharsets.UTF_8)));
   }
 
-  public String checkResponseStatus(String xml, String code) throws Exception {
+  public String checkAdminResponseStatus(String request, int code) throws 
Exception {
     try {
-      String response = query(xml);
-      String valid = validateXPath(response, "//int[@name='status']=" + code);
-      return (null == valid) ? null : response;
-    } catch (XPathExpressionException e) {
-      throw new RuntimeException("?!? static xpath has bug?", e);
-    }
-  }
-
-  public String checkAdminResponseStatus(String xml, String code) throws 
Exception {
-    try {
-      String response = adminQuery(xml);
+      String response = adminQuery(request);
       String valid = validateXPath(response, "//int[@name='status']=" + code);
       return (null == valid) ? null : response;
     } catch (XPathExpressionException e) {
@@ -163,8 +181,7 @@ public class RestTestHarness extends BaseTestHarness 
implements Closeable {
                 adminQuery("/admin/cores?wt=xml&action=STATUS"),
                 "//lst[@name='status']/lst[1]/str[@name='name']",
                 XPathConstants.STRING);
-    String xml =
-        checkAdminResponseStatus("/admin/cores?wt=xml&action=RELOAD&core=" + 
coreName, "0");
+    String xml = 
checkAdminResponseStatus("/admin/cores?wt=xml&action=RELOAD&core=" + coreName, 
0);
     if (null != xml) {
       throw new RuntimeException("RELOAD failed:\n" + xml);
     }
@@ -179,31 +196,52 @@ public class RestTestHarness extends BaseTestHarness 
implements Closeable {
   @Override
   public String update(String xml) {
     try {
-      HttpPost httpPost = new HttpPost(getBaseURL() + "/update");
-      httpPost.setEntity(
-          new StringEntity(xml, ContentType.create("application/xml", 
StandardCharsets.UTF_8)));
-      return getResponse(httpPost);
+      return sendAndReturnString(
+          getHttpClient()
+              .POST(buildUri("/update"))
+              .body(new StringRequestContent("application/xml", xml, 
StandardCharsets.UTF_8)));
+    } catch (RuntimeException e) {
+      throw e;
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
   }
 
-  /** Executes the given request and returns the response. */
-  private String getResponse(HttpUriRequest request) throws IOException {
-    HttpEntity entity = null;
+  private URI buildUri(String request) {
+    return request == null ? getBaseURI() : URLUtil.buildURI(getBaseURI(), 
request);
+  }
+
+  /** Executes the given request and returns the response as a String. */
+  private String sendAndReturnString(Request request) throws IOException {
+    ContentResponse rsp = send(request);
+    return rsp.getContentAsString();
+  }
+
+  private static ContentResponse send(Request request) throws IOException {
+    ContentResponse rsp;
     try {
-      entity =
-          httpClient
-              .execute(request, 
HttpClientUtil.createNewHttpClientRequestContext())
-              .getEntity();
-      return EntityUtils.toString(entity, StandardCharsets.UTF_8);
-    } finally {
-      EntityUtils.consumeQuietly(entity);
+      rsp = request.send();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt(); // reset
+      throw new RuntimeException(e);
+    } catch (TimeoutException e) {
+      throw new RuntimeException(e);
+    } catch (ExecutionException e) {
+      log.warn("Propagating cause of executionException");
+      try {
+        throw e.getCause();
+      } catch (RuntimeException | IOException | Error cause) {
+        throw cause;
+      } catch (Exception cause) {
+        throw new IOException(cause);
+      } catch (Throwable ex) {
+        throw new RuntimeException(ex);
+      }
     }
+    return rsp;
   }
 
-  @Override
-  public void close() throws IOException {
-    HttpClientUtil.close(httpClient);
+  public HttpClient getHttpClient() {
+    return httpClient;
   }
 }

Reply via email to