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

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


The following commit(s) were added to refs/heads/main by this push:
     new f94d30c611c SOLR-17286: Remote Proxy via Jetty HttpClient (#3447)
f94d30c611c is described below

commit f94d30c611c336b631dbb5442b476adb3661c5ca
Author: David Smiley <[email protected]>
AuthorDate: Wed Aug 20 09:32:26 2025 -0400

    SOLR-17286: Remote Proxy via Jetty HttpClient (#3447)
    
    No longer Apache HttpClient.
    
    Rename REMOTEQUERY to REMOTEPROXY
    
    * benchmark module:
    ** Docs: use ExecutorUtil.shutdownAndAwaitTermination
    ** MiniClusterBenchStateTest: do cleanup in @After
---
 .../src/java/org/apache/solr/bench/Docs.java       |   8 +-
 .../solr/bench/MiniClusterBenchStateTest.java      |  23 +--
 solr/core/build.gradle                             |   1 +
 .../src/java/org/apache/solr/api/V2HttpCall.java   |  16 +--
 .../apache/solr/servlet/CoreContainerProvider.java |  12 --
 .../java/org/apache/solr/servlet/HttpSolrCall.java | 157 +++------------------
 .../org/apache/solr/servlet/HttpSolrProxy.java     | 152 ++++++++++++++++++++
 .../apache/solr/servlet/SolrDispatchFilter.java    |  25 ++--
 .../solr/client/solrj/impl/Http2SolrClient.java    |  10 +-
 .../PreemptiveBasicAuthClientBuilderFactory.java   |   7 +-
 10 files changed, 210 insertions(+), 201 deletions(-)

diff --git a/solr/benchmark/src/java/org/apache/solr/bench/Docs.java 
b/solr/benchmark/src/java/org/apache/solr/bench/Docs.java
index d256739acae..b3c28bc1d4b 100644
--- a/solr/benchmark/src/java/org/apache/solr/bench/Docs.java
+++ b/solr/benchmark/src/java/org/apache/solr/bench/Docs.java
@@ -26,13 +26,13 @@ import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 import org.apache.lucene.util.RamUsageEstimator;
 import org.apache.solr.bench.generators.MultiString;
 import org.apache.solr.bench.generators.SolrGen;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.util.CollectionUtil;
+import org.apache.solr.common.util.ExecutorUtil;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
 import org.apache.solr.common.util.SuppressForbidden;
 import org.quicktheories.core.Gen;
@@ -110,11 +110,7 @@ public class Docs {
       executorService.execute(() -> docs.add(Docs.this.inputDocument()));
     }
 
-    executorService.shutdown();
-    boolean result = executorService.awaitTermination(10, TimeUnit.MINUTES);
-    if (!result) {
-      throw new RuntimeException("Timeout waiting for doc adds to finish");
-    }
+    ExecutorUtil.shutdownAndAwaitTermination(executorService);
     log(
         "done preGenerateDocs docs="
             + docs.size()
diff --git 
a/solr/benchmark/src/test/org/apache/solr/bench/MiniClusterBenchStateTest.java 
b/solr/benchmark/src/test/org/apache/solr/bench/MiniClusterBenchStateTest.java
index af568e3bb98..82708f6a34e 100644
--- 
a/solr/benchmark/src/test/org/apache/solr/bench/MiniClusterBenchStateTest.java
+++ 
b/solr/benchmark/src/test/org/apache/solr/bench/MiniClusterBenchStateTest.java
@@ -26,15 +26,13 @@ import static 
org.apache.solr.bench.generators.SourceDSL.longs;
 import static org.apache.solr.bench.generators.SourceDSL.strings;
 
 import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering;
-import java.lang.invoke.MethodHandles;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.concurrent.TimeUnit;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.response.QueryResponse;
-import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.After;
 import org.junit.Test;
 import org.openjdk.jmh.annotations.Mode;
 import org.openjdk.jmh.infra.BenchmarkParams;
@@ -42,12 +40,12 @@ import org.openjdk.jmh.infra.IterationParams;
 import org.openjdk.jmh.runner.IterationType;
 import org.openjdk.jmh.runner.WorkloadParams;
 import org.openjdk.jmh.runner.options.TimeValue;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @ThreadLeakLingering(linger = 10)
 public class MiniClusterBenchStateTest extends SolrTestCaseJ4 {
-  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private MiniClusterState.MiniClusterBenchState miniBenchState;
+  private BaseBenchState baseBenchState;
+  private BenchmarkParams benchParams;
 
   @Test
   public void testMiniClusterState() throws Exception {
@@ -55,9 +53,8 @@ public class MiniClusterBenchStateTest extends SolrTestCaseJ4 
{
     System.setProperty("workBaseDir", createTempDir("work").toString());
     System.setProperty("random.counts", "true");
 
-    MiniClusterState.MiniClusterBenchState miniBenchState =
-        new MiniClusterState.MiniClusterBenchState();
-    BenchmarkParams benchParams =
+    miniBenchState = new MiniClusterState.MiniClusterBenchState();
+    benchParams =
         new BenchmarkParams(
             "benchmark",
             "generatedTarget",
@@ -80,7 +77,7 @@ public class MiniClusterBenchStateTest extends SolrTestCaseJ4 
{
             "vmVersion",
             "jmhVersion",
             TimeValue.seconds(10));
-    BaseBenchState baseBenchState = new BaseBenchState();
+    baseBenchState = new BaseBenchState();
     baseBenchState.doSetup(benchParams);
     miniBenchState.doSetup(benchParams, baseBenchState);
 
@@ -111,7 +108,7 @@ public class MiniClusterBenchStateTest extends 
SolrTestCaseJ4 {
             .field(doubles().all());
 
     int numDocs = 50;
-    Iterator<SolrInputDocument> docIt = docs.preGenerate(numDocs);
+    docs.preGenerate(numDocs);
 
     miniBenchState.index(collection, docs, numDocs);
 
@@ -124,8 +121,12 @@ public class MiniClusterBenchStateTest extends 
SolrTestCaseJ4 {
     BaseBenchState.log("match all query result=" + result);
 
     assertEquals(numDocs, result.getResults().getNumFound());
+  }
 
+  @After
+  public void after() throws Exception {
     BaseBenchState.doTearDown(benchParams);
+
     miniBenchState.tearDown(benchParams);
     miniBenchState.shutdownMiniCluster(benchParams, baseBenchState);
   }
diff --git a/solr/core/build.gradle b/solr/core/build.gradle
index 74a2b9e64d7..0390710a170 100644
--- a/solr/core/build.gradle
+++ b/solr/core/build.gradle
@@ -124,6 +124,7 @@ dependencies {
   implementation libs.eclipse.jetty.http
   implementation libs.eclipse.jetty.io
   implementation libs.eclipse.jetty.toolchain.servletapi
+  implementation libs.eclipse.jetty.util
 
   // ZooKeeper
 
diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java 
b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
index 8c12a95e7b1..51d14b65993 100644
--- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
+++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
@@ -19,9 +19,9 @@ package org.apache.solr.api;
 
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN;
-import static 
org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN_OR_REMOTEQUERY;
+import static 
org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN_OR_REMOTEPROXY;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.PROCESS;
-import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEQUERY;
+import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEPROXY;
 
 import io.opentelemetry.api.trace.Span;
 import jakarta.servlet.http.HttpServletRequest;
@@ -167,8 +167,8 @@ public class V2HttpCall extends HttpSolrCall {
           if (core == null) {
             // this collection exists , but this node does not have a replica 
for that collection
             extractRemotePath(collectionName);
-            if (action == REMOTEQUERY) {
-              action = ADMIN_OR_REMOTEQUERY;
+            if (action == REMOTEPROXY) {
+              action = ADMIN_OR_REMOTEPROXY;
               coreUrl = coreUrl.replace("/solr/", "/solr/____v2/c/");
               this.path = path = path.substring(prefix.length() + 
collectionName.length() + 2);
               return;
@@ -350,7 +350,7 @@ public class V2HttpCall extends HttpSolrCall {
   }
 
   /**
-   * Differentiate between "admin" and "remotequery"-type requests; executing 
each as appropriate.
+   * Differentiate between "admin" and "remoteproxy"-type requests; executing 
each as appropriate.
    *
    * <p>The JAX-RS framework used by {@link V2HttpCall} doesn't provide any 
easy way to check in
    * advance whether a Jersey application can handle an incoming request. 
This, in turn, makes it
@@ -361,7 +361,7 @@ public class V2HttpCall extends HttpSolrCall {
    * <p>This method uses this strategy to differentiate between admin requests 
that don't require a
    * {@link SolrCore}, but whose path happen to contain a core/collection name 
(e.g.
    * ADDREPLICAPROP's path of
-   * /collections/collName/shards/shardName/replicas/replicaName/properties), 
and "REMOTEQUERY"
+   * /collections/collName/shards/shardName/replicas/replicaName/properties), 
and "REMOTEPROXY"
    * requests which do require a local SolrCore to process.
    */
   @Override
@@ -384,8 +384,8 @@ public class V2HttpCall extends HttpSolrCall {
     }
 
     // If no admin/container-level Jersey resource was found for this API, 
then this should be
-    // treated as a REMOTEQUERY
-    sendRemoteQuery();
+    // treated as a REMOTEPROXY
+    sendRemoteProxy();
   }
 
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java 
b/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java
index 8168a4791b2..009bba8dc56 100644
--- a/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java
+++ b/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java
@@ -48,7 +48,6 @@ import javax.naming.Context;
 import javax.naming.InitialContext;
 import javax.naming.NamingException;
 import javax.naming.NoInitialContextException;
-import org.apache.http.client.HttpClient;
 import org.apache.lucene.store.MMapDirectory;
 import org.apache.lucene.util.VectorUtil;
 import org.apache.solr.client.api.util.SolrVersion;
@@ -83,7 +82,6 @@ public class CoreContainerProvider implements 
ServletContextListener {
   private final String metricTag = SolrMetricProducer.getUniqueMetricTag(this, 
null);
   private CoreContainer cores;
   private Properties extraProperties;
-  private HttpClient httpClient;
   private SolrMetricManager metricManager;
   private RateLimitManager rateLimitManager;
   private String registryName;
@@ -122,14 +120,6 @@ public class CoreContainerProvider implements 
ServletContextListener {
     return cores;
   }
 
-  /**
-   * @see SolrDispatchFilter#getHttpClient()
-   */
-  HttpClient getHttpClient() throws UnavailableException {
-    checkReady();
-    return httpClient;
-  }
-
   private void checkReady() throws UnavailableException {
     // TODO throw AlreadyClosedException instead?
     if (cores == null) {
@@ -175,7 +165,6 @@ public class CoreContainerProvider implements 
ServletContextListener {
       }
     } finally {
       if (cc != null) {
-        httpClient = null;
         cc.shutdown();
       }
     }
@@ -227,7 +216,6 @@ public class CoreContainerProvider implements 
ServletContextListener {
               });
 
       coresInit = createCoreContainer(computeSolrHome(servletContext), 
extraProperties);
-      this.httpClient = 
coresInit.getUpdateShardHandler().getDefaultHttpClient();
       setupJvmMetrics(coresInit, coresInit.getNodeConfig().getMetricsConfig());
 
       SolrZkClient zkClient = null;
diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java 
b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
index 8107e36a4cd..4b4ca5d9619 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -23,7 +23,7 @@ import static 
org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.FORWARD;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.PASSTHROUGH;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.PROCESS;
-import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEQUERY;
+import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEPROXY;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.RETRY;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.RETURN;
 
@@ -32,15 +32,11 @@ import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.EOFException;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.lang.invoke.MethodHandles;
 import java.nio.charset.StandardCharsets;
-import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
@@ -53,26 +49,10 @@ import java.util.Random;
 import java.util.Set;
 import java.util.function.Supplier;
 import net.jcip.annotations.ThreadSafe;
-import org.apache.http.Header;
-import org.apache.http.HeaderIterator;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpEntityEnclosingRequest;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpHead;
-import org.apache.http.client.methods.HttpOptions;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.entity.InputStreamEntity;
 import org.apache.solr.api.ApiBag;
 import org.apache.solr.api.V2HttpCall;
 import org.apache.solr.client.api.util.SolrVersion;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.impl.HttpClientUtil;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.Aliases;
@@ -121,6 +101,7 @@ import 
org.apache.solr.update.processor.DistributingUpdateProcessorFactory;
 import org.apache.solr.util.RTimerTree;
 import org.apache.solr.util.tracing.TraceUtils;
 import org.apache.zookeeper.KeeperException;
+import org.eclipse.jetty.client.HttpClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.MarkerFactory;
@@ -130,8 +111,6 @@ import org.slf4j.MarkerFactory;
 public class HttpSolrCall {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  public static final String ORIGINAL_USER_PRINCIPAL_HEADER = 
"originalUserPrincipal";
-
   public static final String INTERNAL_REQUEST_COUNT = "_forwardedCount";
 
   protected final SolrDispatchFilter solrDispatchFilter;
@@ -287,7 +266,7 @@ public class HttpSolrCall {
           // if we couldn't find it locally, look on other nodes
           if (idx > 0) {
             extractRemotePath(collectionName);
-            if (action == REMOTEQUERY) {
+            if (action == REMOTEPROXY) {
               path = path.substring(idx);
               return;
             }
@@ -417,7 +396,7 @@ public class HttpSolrCall {
             SolrException.ErrorCode.INVALID_STATE,
             new String(Utils.toJSON(invalidStates), StandardCharsets.UTF_8));
       }
-      action = REMOTEQUERY;
+      action = REMOTEPROXY;
     } else {
       if (!retry) {
         // we couldn't find a core to work with, try reloading aliases & this 
collection
@@ -436,12 +415,6 @@ public class HttpSolrCall {
     }
   }
 
-  protected void sendRemoteQuery() throws IOException {
-    SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new 
SolrQueryResponse(), action));
-    mustClearSolrRequestInfo = true;
-    remoteQuery(coreUrl + path, response);
-  }
-
   /** This method processes the request. */
   public Action call() throws IOException {
 
@@ -475,7 +448,7 @@ public class HttpSolrCall {
       // able to perform the authorization.
       if (cores.getAuthorizationPlugin() != null
           && shouldAuthorize()
-          && !(action == REMOTEQUERY || action == FORWARD)) {
+          && !(action == REMOTEPROXY || action == FORWARD)) {
         final AuthorizationContext authzContext = getAuthCtx();
         AuthorizationUtils.AuthorizationFailure authzFailure =
             AuthorizationUtils.authorize(req, response, cores, authzContext);
@@ -487,14 +460,14 @@ public class HttpSolrCall {
 
       HttpServletResponse resp = response;
       switch (action) {
-        case ADMIN_OR_REMOTEQUERY:
+        case ADMIN_OR_REMOTEPROXY:
           handleAdminOrRemoteRequest();
           return RETURN;
         case ADMIN:
           handleAdminRequest();
           return RETURN;
-        case REMOTEQUERY:
-          sendRemoteQuery();
+        case REMOTEPROXY:
+          sendRemoteProxy();
           return RETURN;
         case PROCESS:
           final Method reqMethod = Method.getMethod(req.getMethod());
@@ -566,7 +539,7 @@ public class HttpSolrCall {
 
   /**
    * Handle a request whose "type" could not be discerned in advance and may 
be either "admin" or
-   * "remotequery".
+   * "remoteproxy".
    *
    * <p>Some implementations (such as {@link V2HttpCall}) may find it 
difficult to differentiate all
    * request types in advance. This method serves as a hook; allowing those 
implementations to
@@ -661,107 +634,20 @@ public class HttpSolrCall {
     }
   }
 
-  // TODO using Http2Client
-  private void remoteQuery(String coreUrl, HttpServletResponse resp) throws 
IOException {
-    HttpRequestBase method;
-    HttpEntity httpEntity = null;
-
+  protected void sendRemoteProxy() throws IOException {
     ModifiableSolrParams updatedQueryParams = new 
ModifiableSolrParams(queryParams);
     int forwardCount = queryParams.getInt(INTERNAL_REQUEST_COUNT, 0) + 1;
     updatedQueryParams.set(INTERNAL_REQUEST_COUNT, forwardCount);
     String queryStr = updatedQueryParams.toQueryString();
 
+    String coreUrlAndPath = coreUrl + path;
+    log.info("Proxying request to: {}", coreUrlAndPath);
     try {
-      String urlstr = coreUrl + queryStr;
-
-      boolean isPostOrPutRequest = "POST".equals(req.getMethod()) || 
"PUT".equals(req.getMethod());
-      if ("GET".equals(req.getMethod())) {
-        method = new HttpGet(urlstr);
-      } else if ("HEAD".equals(req.getMethod())) {
-        method = new HttpHead(urlstr);
-      } else if (isPostOrPutRequest) {
-        HttpEntityEnclosingRequestBase entityRequest =
-            "POST".equals(req.getMethod()) ? new HttpPost(urlstr) : new 
HttpPut(urlstr);
-        InputStream in = req.getInputStream();
-        HttpEntity entity = new InputStreamEntity(in, req.getContentLength());
-        entityRequest.setEntity(entity);
-        method = entityRequest;
-      } else if ("DELETE".equals(req.getMethod())) {
-        method = new HttpDelete(urlstr);
-      } else if ("OPTIONS".equals(req.getMethod())) {
-        method = new HttpOptions(urlstr);
-      } else {
-        throw new SolrException(
-            SolrException.ErrorCode.SERVER_ERROR, "Unexpected method type: " + 
req.getMethod());
-      }
-
-      for (Enumeration<String> e = req.getHeaderNames(); e.hasMoreElements(); 
) {
-        String headerName = e.nextElement();
-        if (!"host".equalsIgnoreCase(headerName)
-            && !"authorization".equalsIgnoreCase(headerName)
-            && !"accept".equalsIgnoreCase(headerName)) {
-          method.addHeader(headerName, req.getHeader(headerName));
-        }
-      }
-      // These headers not supported for HttpEntityEnclosingRequests
-      if (method instanceof HttpEntityEnclosingRequest) {
-        method.removeHeaders(TRANSFER_ENCODING_HEADER);
-        method.removeHeaders(CONTENT_LENGTH_HEADER);
-      }
-
-      // Make sure the user principal is forwarded when its exist
-      HttpClientContext httpClientRequestContext =
-          HttpClientUtil.createNewHttpClientRequestContext();
-      Principal userPrincipal = req.getUserPrincipal();
-      if (userPrincipal != null) {
-        // Normally the context contains a static userToken to enable reuse 
resources. However, if a
-        // personal Principal object exists, we use that instead, also as a 
means to transfer
-        // authentication information to Auth plugins that wish to intercept 
the request later
-        if (log.isDebugEnabled()) {
-          log.debug("Forwarding principal {}", userPrincipal);
-        }
-        httpClientRequestContext.setUserToken(userPrincipal);
-      }
-
-      // Execute the method.
-      final HttpResponse response =
-          solrDispatchFilter.getHttpClient().execute(method, 
httpClientRequestContext);
-      int httpStatus = response.getStatusLine().getStatusCode();
-      httpEntity = response.getEntity();
-
-      resp.setStatus(httpStatus);
-      for (HeaderIterator responseHeaders = response.headerIterator();
-          responseHeaders.hasNext(); ) {
-        Header header = responseHeaders.nextHeader();
-
-        // We pull out these two headers below because they can cause chunked
-        // encoding issues with Tomcat
-        if (header != null
-            && !header.getName().equalsIgnoreCase(TRANSFER_ENCODING_HEADER)
-            && !header.getName().equalsIgnoreCase(CONNECTION_HEADER)) {
-
-          // NOTE: explicitly using 'setHeader' instead of 'addHeader' so that
-          // the remote nodes values for any response headers will overide any 
that
-          // may have already been set locally (ex: by the local jetty's 
RewriteHandler config)
-          resp.setHeader(header.getName(), header.getValue());
-        }
-      }
-
-      if (httpEntity != null) {
-        if (httpEntity.getContentEncoding() != null)
-          resp.setHeader(
-              httpEntity.getContentEncoding().getName(),
-              httpEntity.getContentEncoding().getValue());
-        if (httpEntity.getContentType() != null)
-          resp.setContentType(httpEntity.getContentType().getValue());
-
-        InputStream is = httpEntity.getContent();
-        OutputStream os = resp.getOutputStream();
-
-        is.transferTo(os);
-      }
-
-    } catch (IOException e) {
+      response.reset(); // clear all headers and status
+      HttpClient httpClient = cores.getDefaultHttpSolrClient().getHttpClient();
+      HttpSolrProxy.doHttpProxy(httpClient, req, response, coreUrlAndPath + 
queryStr);
+    } catch (Exception e) {
+      // note: don't handle interruption differently; we are stopping
       sendError(
           new SolrException(
               SolrException.ErrorCode.SERVER_ERROR,
@@ -770,8 +656,10 @@ public class HttpSolrCall {
                   + " with _forwardCount: "
                   + forwardCount,
               e));
-    } finally {
-      Utils.consumeFully(httpEntity);
+    } catch (Error e) {
+      throw e;
+    } catch (Throwable e) {
+      throw new RuntimeException(e);
     }
   }
 
@@ -1209,9 +1097,6 @@ public class HttpSolrCall {
     };
   }
 
-  static final String CONNECTION_HEADER = "Connection";
-  static final String TRANSFER_ENCODING_HEADER = "Transfer-Encoding";
-  static final String CONTENT_LENGTH_HEADER = "Content-Length";
   List<CommandOperation> parsedCommands;
 
   public List<CommandOperation> getCommands(boolean validateInput) {
diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrProxy.java 
b/solr/core/src/java/org/apache/solr/servlet/HttpSolrProxy.java
new file mode 100644
index 00000000000..153a97d36ed
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrProxy.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.servlet;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.apache.solr.util.tracing.TraceUtils;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.InputStreamRequestContent;
+import org.eclipse.jetty.client.Request;
+import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.client.Result;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+
+/** Helper class for proxying the request to another Solr node. */
+// Tried to use Jetty's ProxyServlet instead but ran into inexplicable 
difficulties:  EOF/reset.
+// Perhaps was related to its use of ServletRequest.startAsync()/AsyncContext
+class HttpSolrProxy {
+  // TODO add X-Forwarded-For and with comma delimited
+
+  private static final Set<HttpHeader> HOP_BY_HOP_HEADERS =
+      EnumSet.of(
+          HttpHeader.CONNECTION,
+          HttpHeader.KEEP_ALIVE,
+          HttpHeader.PROXY_AUTHENTICATE,
+          HttpHeader.PROXY_AUTHORIZATION,
+          HttpHeader.TE,
+          HttpHeader.TRANSFER_ENCODING,
+          HttpHeader.UPGRADE);
+
+  // Methods that shouldn't have a body according to HTTP spec
+  private static final Set<String> NO_BODY_METHODS = Set.of("GET", "HEAD", 
"DELETE");
+
+  static void doHttpProxy(
+      HttpClient httpClient,
+      HttpServletRequest servletReq,
+      HttpServletResponse servletRsp,
+      String url)
+      throws Throwable {
+    Request proxyReq = 
httpClient.newRequest(url).method(servletReq.getMethod());
+
+    // clearing them first to ensure there's no stock entries (e.g. user-agent)
+    proxyReq.headers(proxyFields -> copyRequestHeaders(servletReq, 
proxyFields.clear()));
+
+    // FYI see InstrumentedHttpListenerFactory
+    TraceUtils.injectTraceContext(proxyReq);
+    // TODO client spans.  See OTEL agent's approach:
+    // 
https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/jetty-httpclient/jetty-httpclient-12.0
+
+    if (!NO_BODY_METHODS.contains(servletReq.getMethod())) {
+      proxyReq.body(
+          new InputStreamRequestContent(servletReq.getContentType(), 
servletReq.getInputStream()));
+    }
+
+    CompletableFuture<Result> resultFuture = new CompletableFuture<>();
+
+    proxyReq.send(
+        new Response.Listener() {
+          private final byte[] buffer = new byte[8192];
+
+          @Override
+          public void onBegin(Response response) {
+            servletRsp.setStatus(response.getStatus());
+          }
+
+          @Override
+          public void onHeaders(Response response) {
+            copyResponseHeaders(response, servletRsp);
+          }
+
+          @Override
+          public void onContent(Response response, ByteBuffer content) {
+            try {
+              final OutputStream clientOutputStream = 
servletRsp.getOutputStream();
+
+              // Copy content to the client's output stream in chunks using 
the existing buffer
+              int remaining = content.remaining();
+              while (remaining > 0) {
+                int chunkSize = Math.min(remaining, buffer.length);
+                content.get(buffer, 0, chunkSize);
+                clientOutputStream.write(buffer, 0, chunkSize);
+                remaining -= chunkSize;
+              }
+              clientOutputStream.flush();
+            } catch (IOException e) {
+              throw new RuntimeException(e);
+            }
+          }
+
+          @Override
+          public void onComplete(Result result) {
+            resultFuture.complete(result);
+          }
+        });
+
+    Result result = resultFuture.get(); // waits
+    var failure = result.getFailure();
+    if (failure != null) {
+      throw failure;
+    }
+  }
+
+  private static void copyRequestHeaders(
+      HttpServletRequest servletReq, HttpFields.Mutable proxyFields) {
+    servletReq
+        .getHeaderNames()
+        .asIterator()
+        .forEachRemaining(
+            headerName -> {
+              HttpHeader knownHeader = HttpHeader.CACHE.get(headerName); // 
maybe null
+              if (!HOP_BY_HOP_HEADERS.contains(knownHeader)) {
+                servletReq
+                    .getHeaders(headerName)
+                    .asIterator()
+                    .forEachRemaining(headerVal -> proxyFields.add(headerName, 
headerVal));
+              }
+            });
+  }
+
+  private static void copyResponseHeaders(Response proxyRsp, 
HttpServletResponse servletRsp) {
+    for (HttpField headerField : proxyRsp.getHeaders()) {
+      HttpHeader knownHeader = headerField.getHeader();
+      if (!HOP_BY_HOP_HEADERS.contains(knownHeader)) {
+        // HttpField: even if multiple values, it's encoded as one comma 
delimited value
+        servletRsp.addHeader(headerField.getName(), headerField.getValue());
+      }
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java 
b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
index 36e86a2c235..b62479bad77 100644
--- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
@@ -37,7 +37,6 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
-import org.apache.http.client.HttpClient;
 import org.apache.solr.api.V2HttpCall;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
@@ -88,15 +87,6 @@ public class SolrDispatchFilter extends HttpFilter 
implements PathExcluder {
 
   public final boolean isV2Enabled = V2ApiUtils.isEnabled();
 
-  public HttpClient getHttpClient() {
-    try {
-      return containerProvider.getHttpClient();
-    } catch (UnavailableException e) {
-      throw new SolrException(
-          ErrorCode.SERVER_ERROR, "Internal Http Client Unavailable, startup 
may have failed");
-    }
-  }
-
   /**
    * Enum to define action that needs to be processed. PASSTHROUGH: Pass 
through to another filter
    * via webapp. FORWARD: Forward rewritten URI (without path prefix and 
core/collection name) to
@@ -110,9 +100,9 @@ public class SolrDispatchFilter extends HttpFilter 
implements PathExcluder {
     RETURN,
     RETRY,
     ADMIN,
-    REMOTEQUERY,
+    REMOTEPROXY,
     PROCESS,
-    ADMIN_OR_REMOTEQUERY
+    ADMIN_OR_REMOTEPROXY
   }
 
   public SolrDispatchFilter() {}
@@ -131,6 +121,7 @@ public class SolrDispatchFilter extends HttpFilter 
implements PathExcluder {
 
   @Override
   public void init(FilterConfig config) throws ServletException {
+    super.init(config);
     try {
       containerProvider = 
CoreContainerProvider.serviceForContext(config.getServletContext());
       boolean isCoordinator =
@@ -197,9 +188,11 @@ public class SolrDispatchFilter extends HttpFilter 
implements PathExcluder {
             }
           });
     } finally {
-      ServletUtils.consumeInputFully(request, response);
       SolrRequestInfo.reset();
-      SolrRequestParsers.cleanupMultipartFiles(request);
+      if (!request.isAsyncStarted()) { // jetty's proxy uses this
+        ServletUtils.consumeInputFully(request, response);
+        SolrRequestParsers.cleanupMultipartFiles(request);
+      }
     }
   }
 
@@ -246,8 +239,8 @@ public class SolrDispatchFilter extends HttpFilter 
implements PathExcluder {
           break;
         case ADMIN:
         case PROCESS:
-        case REMOTEQUERY:
-        case ADMIN_OR_REMOTEQUERY:
+        case REMOTEPROXY:
+        case ADMIN_OR_REMOTEPROXY:
         case RETURN:
           break;
       }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
index 4535013a72b..7ba07fe9ffb 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
@@ -69,7 +69,6 @@ import org.eclipse.jetty.client.MultiPartRequestContent;
 import org.eclipse.jetty.client.Origin.Address;
 import org.eclipse.jetty.client.Origin.Protocol;
 import org.eclipse.jetty.client.OutputStreamRequestContent;
-import org.eclipse.jetty.client.ProtocolHandlers;
 import org.eclipse.jetty.client.ProxyConfiguration;
 import org.eclipse.jetty.client.Request;
 import org.eclipse.jetty.client.Response;
@@ -210,16 +209,11 @@ public class Http2SolrClient extends HttpSolrClientBase {
     this.listenerFactory.add(factory);
   }
 
-  // internal usage only
-  HttpClient getHttpClient() {
+  /** internal use only */
+  public HttpClient getHttpClient() {
     return httpClient;
   }
 
-  // internal usage only
-  ProtocolHandlers getProtocolHandlers() {
-    return httpClient.getProtocolHandlers();
-  }
-
   private HttpClient createHttpClient(Builder builder) {
     executor = builder.executor;
     if (executor == null) {
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/PreemptiveBasicAuthClientBuilderFactory.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/PreemptiveBasicAuthClientBuilderFactory.java
index 1ba61fa9137..d58f645b38a 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/PreemptiveBasicAuthClientBuilderFactory.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/PreemptiveBasicAuthClientBuilderFactory.java
@@ -95,10 +95,9 @@ public class PreemptiveBasicAuthClientBuilderFactory 
implements HttpClientBuilde
     authenticationStore.addAuthentication(
         new SolrBasicAuthentication(basicAuthUser, basicAuthPass));
     client.setAuthenticationStore(authenticationStore);
-    client.getProtocolHandlers().put(new 
WWWAuthenticationProtocolHandler(client.getHttpClient()));
-    client
-        .getProtocolHandlers()
-        .put(new ProxyAuthenticationProtocolHandler(client.getHttpClient()));
+    var httpClient = client.getHttpClient();
+    httpClient.getProtocolHandlers().put(new 
WWWAuthenticationProtocolHandler(httpClient));
+    httpClient.getProtocolHandlers().put(new 
ProxyAuthenticationProtocolHandler(httpClient));
   }
 
   @Override


Reply via email to