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