You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2022/10/24 12:11:35 UTC

[solr] branch branch_9x updated: SOLR-16392: Support REST-ful path lookup in V2HttpCall

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

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


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 9eab083b64b SOLR-16392: Support REST-ful path lookup in V2HttpCall
9eab083b64b is described below

commit 9eab083b64b84f0a1158525fab70323c51d5c869
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Sep 29 15:38:05 2022 -0400

    SOLR-16392: Support REST-ful path lookup in V2HttpCall
    
    Prior to this commit, V2HttpCall uses a rather crude heuristic to
    determine whether a given API is a "container-level" or "core-level"
    API.  Namely, any path that a core/collection name can be parsed out of
    is a "core" API that should live in the core-level PluginBag.
    Conversely, anything else is a container-level API that should live in
    the container-level PluginBag.
    
    This happened to work for V2 because we initially chose our API paths
    with this heuristic in mind.  But the result is an API that's neither intuitive
    nor user-friendly.
    
    This PR takes a sort of "guess and check" approach, where we try
    serving ambiguous requests from multiple Jersey apps in series, stopping
    at the first one that doesn't spit out a quick NotFoundException.
---
 .../src/java/org/apache/solr/api/V2HttpCall.java   | 113 +++++++++++++++++----
 .../solr/jersey/CatchAllExceptionMapper.java       |  28 ++---
 .../org/apache/solr/jersey/JerseyApplications.java |   1 +
 .../solr/jersey/NotFoundExceptionMapper.java       |  78 ++++++++++++++
 .../solr/jersey/PostRequestDecorationFilter.java   |   3 +
 .../org/apache/solr/jersey/RequestContextKeys.java |  19 ++++
 .../apache/solr/jersey/RequestMetricHandling.java  |   4 +
 .../java/org/apache/solr/servlet/HttpSolrCall.java |  33 +++++-
 .../apache/solr/servlet/SolrDispatchFilter.java    |   4 +-
 9 files changed, 247 insertions(+), 36 deletions(-)

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 d8366f4424a..51db1ce6d10 100644
--- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
+++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
@@ -19,6 +19,7 @@ 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.PROCESS;
 import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEQUERY;
 
@@ -143,7 +144,6 @@ public class V2HttpCall extends HttpSolrCall {
 
         DocCollection collection =
             resolveDocCollection(queryParams.get(COLLECTION_PROP, origCorename));
-
         if (collection == null) {
           if (!path.endsWith(CommonParams.INTROSPECT)) {
             throw new SolrException(
@@ -156,6 +156,7 @@ public class V2HttpCall extends HttpSolrCall {
             // this collection exists , but this node does not have a replica for that collection
             extractRemotePath(collection.getName(), collection.getName());
             if (action == REMOTEQUERY) {
+              action = ADMIN_OR_REMOTEQUERY;
               coreUrl = coreUrl.replace("/solr/", "/solr/____v2/c/");
               this.path =
                   path = path.substring(prefix.length() + collection.getName().length() + 2);
@@ -334,32 +335,82 @@ public class V2HttpCall extends HttpSolrCall {
     }
   }
 
-  private void invokeJerseyRequest(
-      CoreContainer cores, SolrCore core, ApplicationHandler jerseyHandler, SolrQueryResponse rsp) {
-    try {
-      final ContainerRequest containerRequest =
-          ContainerRequestUtils.createContainerRequest(
-              req, response, jerseyHandler.getConfiguration());
-
-      // Set properties that may be used by Jersey filters downstream
-      containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_REQUEST, solrReq);
-      containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_RESPONSE, rsp);
-      containerRequest.setProperty(RequestContextKeys.CORE_CONTAINER, cores);
-      containerRequest.setProperty(RequestContextKeys.HTTP_SERVLET_REQ, req);
-      containerRequest.setProperty(RequestContextKeys.REQUEST_TYPE, requestType);
-      containerRequest.setProperty(RequestContextKeys.SOLR_PARAMS, queryParams);
-      containerRequest.setProperty(RequestContextKeys.COLLECTION_LIST, collectionsList);
-      containerRequest.setProperty(RequestContextKeys.HTTP_SERVLET_RSP, response);
-      if (core != null) {
-        containerRequest.setProperty(RequestContextKeys.SOLR_CORE, core);
+  private boolean invokeJerseyRequest(
+      CoreContainer cores, SolrCore core, ApplicationHandler primary, SolrQueryResponse rsp) {
+    return invokeJerseyRequest(cores, core, primary, rsp, Map.of());
+  }
+
+  private boolean invokeJerseyRequest(
+      CoreContainer cores,
+      SolrCore core,
+      ApplicationHandler jerseyHandler,
+      SolrQueryResponse rsp,
+      Map<String, String> additionalProperties) {
+    final ContainerRequest containerRequest =
+        ContainerRequestUtils.createContainerRequest(
+            req, response, jerseyHandler.getConfiguration());
+
+    // Set properties that may be used by Jersey filters downstream
+    containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_REQUEST, solrReq);
+    containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_RESPONSE, rsp);
+    containerRequest.setProperty(RequestContextKeys.CORE_CONTAINER, cores);
+    containerRequest.setProperty(RequestContextKeys.HTTP_SERVLET_REQ, req);
+    containerRequest.setProperty(RequestContextKeys.REQUEST_TYPE, requestType);
+    containerRequest.setProperty(RequestContextKeys.SOLR_PARAMS, queryParams);
+    containerRequest.setProperty(RequestContextKeys.COLLECTION_LIST, collectionsList);
+    containerRequest.setProperty(RequestContextKeys.HTTP_SERVLET_RSP, response);
+    if (core != null) {
+      containerRequest.setProperty(RequestContextKeys.SOLR_CORE, core);
+    }
+    if (additionalProperties != null) {
+      for (Map.Entry<String, String> entry : additionalProperties.entrySet()) {
+        containerRequest.setProperty(entry.getKey(), entry.getValue());
       }
+    }
+
+    try {
       servedByJaxRs = true;
       jerseyHandler.handle(containerRequest);
+      return containerRequest.getProperty(RequestContextKeys.NOT_FOUND_FLAG) == null;
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
   }
 
+  /**
+   * Differentiate between "admin" and "remotequery"-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
+   * difficult to classify requests as being "admin" or "core, "local" or "remote". The only option
+   * is to submit the request to the JAX-RS application and see whether a quick "404" flag comes
+   * back, or not.
+   *
+   * <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"
+   * requests which do require a local SolrCore to process.
+   */
+  @Override
+  protected void handleAdminOrRemoteRequest() throws IOException {
+
+    final Map<String, String> suppressNotFoundProp =
+        Map.of(RequestContextKeys.SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION, "true");
+    SolrQueryResponse solrResp = new SolrQueryResponse();
+    final boolean jerseyResourceFound =
+        invokeJerseyRequest(
+            cores, null, cores.getJerseyApplicationHandler(), solrResp, suppressNotFoundProp);
+    if (jerseyResourceFound) {
+      logAndFlushAdminRequest(solrResp);
+      return;
+    }
+
+    // If no admin/container-level Jersey resource was found for this API, then this should be
+    // treated as a REMOTEQUERY
+    sendRemoteQuery();
+  }
+
   @Override
   protected void handleAdmin(SolrQueryResponse solrResp) {
     if (api == null) {
@@ -376,10 +427,32 @@ public class V2HttpCall extends HttpSolrCall {
     }
   }
 
+  /**
+   * Executes the API or Jersey resource corresponding to a core-level request.
+   *
+   * <p>{@link Api}-based endpoints do this by invoking {@link Api#call(SolrQueryRequest,
+   * SolrQueryResponse)}.
+   *
+   * <p>JAX-RS-based endpoints must check both the core-level and container-level JAX-RS
+   * applications as the resource for a given "core-level request" might be registered in either
+   * place, depending on various legacy factors like the request handler it is associated with. In
+   * support of this, the JAX-RS codepath sets a flag to suppress the normal 404 error response when
+   * checking the first of the two JAX-RS applications.
+   *
+   * @see org.apache.solr.jersey.NotFoundExceptionMapper
+   */
   @Override
   protected void executeCoreRequest(SolrQueryResponse rsp) {
     if (api == null) {
-      invokeJerseyRequest(cores, core, core.getJerseyApplicationHandler(), rsp);
+      final Map<String, String> suppressNotFoundProp =
+          Map.of(RequestContextKeys.SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION, "true");
+      final boolean resourceFound =
+          invokeJerseyRequest(
+              cores, core, core.getJerseyApplicationHandler(), rsp, suppressNotFoundProp);
+      if (!resourceFound) {
+        response.getHeaderNames().stream().forEach(name -> response.setHeader(name, null));
+        invokeJerseyRequest(cores, null, cores.getJerseyApplicationHandler(), rsp);
+      }
     } else {
       SolrCore.preDecorateResponse(solrReq, rsp);
       try {
diff --git a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
index 6d08f7fe86a..2ff8253216d 100644
--- a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
@@ -26,7 +26,6 @@ import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_REQUEST;
 import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_RESPONSE;
 
 import java.lang.invoke.MethodHandles;
-import javax.ws.rs.NotFoundException;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.container.ResourceContext;
@@ -34,7 +33,6 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.ExceptionMapper;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -66,15 +64,7 @@ public class CatchAllExceptionMapper implements ExceptionMapper<Exception> {
         (SolrQueryResponse) containerRequestContext.getProperty(SOLR_QUERY_RESPONSE);
     final SolrQueryRequest solrQueryRequest =
         (SolrQueryRequest) containerRequestContext.getProperty(SOLR_QUERY_REQUEST);
-    if (exception instanceof NotFoundException) {
-      // For backwards compatibility with existing v2 API format
-      exception =
-          new SolrException(
-              SolrException.ErrorCode.NOT_FOUND,
-              "Cannot find API for the path: "
-                  + solrQueryRequest.getContext().get(CommonParams.PATH));
-      solrQueryResponse.setException(exception);
-    } else if (exception instanceof WebApplicationException) {
+    if (exception instanceof WebApplicationException) {
       final WebApplicationException wae = (WebApplicationException) exception;
       final SolrException solrException =
           new SolrException(getErrorCode(wae.getResponse().getStatus()), wae.getMessage());
@@ -88,6 +78,13 @@ public class CatchAllExceptionMapper implements ExceptionMapper<Exception> {
       return processWebApplicationException((WebApplicationException) exception);
     }
 
+    return processAndRespondToException(exception, solrQueryRequest, containerRequestContext);
+  }
+
+  public static Response processAndRespondToException(
+      Exception exception,
+      SolrQueryRequest solrQueryRequest,
+      ContainerRequestContext containerRequestContext) {
     // First, handle any exception-related metrics
     final Exception normalizedException =
         RequestHandlerBase.normalizeReceivedException(solrQueryRequest, exception);
@@ -99,6 +96,13 @@ public class CatchAllExceptionMapper implements ExceptionMapper<Exception> {
 
     // Then, convert the exception into a SolrJerseyResponse (creating one as necessary
     // if response not found, etc.)
+    return buildExceptionResponse(normalizedException, solrQueryRequest, containerRequestContext);
+  }
+
+  public static Response buildExceptionResponse(
+      Exception normalizedException,
+      SolrQueryRequest solrQueryRequest,
+      ContainerRequestContext containerRequestContext) {
     final SolrJerseyResponse response =
         containerRequestContext.getProperty(SOLR_JERSEY_RESPONSE) == null
             ? new SolrJerseyResponse()
@@ -110,7 +114,7 @@ public class CatchAllExceptionMapper implements ExceptionMapper<Exception> {
     return Response.status(response.error.code).type(mediaType).entity(response).build();
   }
 
-  private String getMediaType(SolrQueryRequest solrQueryRequest) {
+  private static String getMediaType(SolrQueryRequest solrQueryRequest) {
     final String wtParam = solrQueryRequest.getParams().get(WT);
     if (wtParam == null) return "application/json";
 
diff --git a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
index 9fe77f07c09..582c1f9fc01 100644
--- a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
+++ b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
@@ -56,6 +56,7 @@ public class JerseyApplications {
 
       // Request lifecycle logic
       register(CatchAllExceptionMapper.class);
+      register(NotFoundExceptionMapper.class);
       register(RequestMetricHandling.PreRequestMetricsFilter.class);
       register(RequestMetricHandling.PostRequestMetricsFilter.class);
       register(PostRequestDecorationFilter.class);
diff --git a/solr/core/src/java/org/apache/solr/jersey/NotFoundExceptionMapper.java b/solr/core/src/java/org/apache/solr/jersey/NotFoundExceptionMapper.java
new file mode 100644
index 00000000000..36e4ab0a407
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/jersey/NotFoundExceptionMapper.java
@@ -0,0 +1,78 @@
+/*
+ * 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.jersey;
+
+import static org.apache.solr.jersey.CatchAllExceptionMapper.processAndRespondToException;
+import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_REQUEST;
+import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_RESPONSE;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ResourceContext;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import org.apache.solr.api.V2HttpCall;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * An {@link ExceptionMapper} for the exception produced by JAX-RS when no match could be found for
+ * an incoming request.
+ *
+ * <p>If no special flags are set, this mapper returns an error response similar to that produced by
+ * other V2 codepaths.
+ *
+ * <p>If the {@link RequestContextKeys#SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION} flag is present, we
+ * suppress the error response and merely set a flag indicating the lack of matching resource. This
+ * is done in service of fielding requests whose resource might be registered in one of multiple
+ * JAX-RS applications. See {@link V2HttpCall}'s "executeCoreRequest" method for more details.
+ */
+public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
+
+  @Context public ResourceContext resourceContext;
+
+  @Override
+  public Response toResponse(NotFoundException exception) {
+    final ContainerRequestContext containerRequestContext =
+        resourceContext.getResource(ContainerRequestContext.class);
+
+    if (containerRequestContext
+        .getPropertyNames()
+        .contains(RequestContextKeys.SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION)) {
+      containerRequestContext.setProperty(RequestContextKeys.NOT_FOUND_FLAG, "NOT_FOUND_VALUE");
+      return null;
+    }
+
+    final SolrQueryResponse solrQueryResponse =
+        (SolrQueryResponse) containerRequestContext.getProperty(SOLR_QUERY_RESPONSE);
+    final SolrQueryRequest solrQueryRequest =
+        (SolrQueryRequest) containerRequestContext.getProperty(SOLR_QUERY_REQUEST);
+    final SolrException stashedException =
+        new SolrException(
+            SolrException.ErrorCode.NOT_FOUND,
+            "Cannot find API for the path: "
+                + solrQueryRequest.getContext().get(CommonParams.PATH));
+    solrQueryResponse.setException(stashedException);
+
+    return processAndRespondToException(
+        stashedException, solrQueryRequest, containerRequestContext);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/jersey/PostRequestDecorationFilter.java b/solr/core/src/java/org/apache/solr/jersey/PostRequestDecorationFilter.java
index 3f5dd3291ac..8735368f879 100644
--- a/solr/core/src/java/org/apache/solr/jersey/PostRequestDecorationFilter.java
+++ b/solr/core/src/java/org/apache/solr/jersey/PostRequestDecorationFilter.java
@@ -45,6 +45,9 @@ public class PostRequestDecorationFilter implements ContainerResponseFilter {
   public void filter(
       ContainerRequestContext requestContext, ContainerResponseContext responseContext)
       throws IOException {
+    if (requestContext.getPropertyNames().contains(RequestContextKeys.NOT_FOUND_FLAG)) {
+      return;
+    }
     final SolrQueryRequest solrQueryRequest =
         (SolrQueryRequest) requestContext.getProperty(SOLR_QUERY_REQUEST);
     if (!responseContext.hasEntity()
diff --git a/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java b/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java
index 18a2aad2865..68b37aa8758 100644
--- a/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java
+++ b/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java
@@ -49,4 +49,23 @@ public interface RequestContextKeys {
   String HANDLER_METRICS = RequestHandlerBase.HandlerMetrics.class.getName();
   String TIMER = Timer.Context.class.getName();
   String SOLR_JERSEY_RESPONSE = SolrJerseyResponse.class.getName();
+
+  /**
+   * A flag read by {@link NotFoundExceptionMapper} to suppress its normal error response
+   *
+   * <p>Used primarily to allow Solr to lookup certain APIs in multiple JAX-RS applications.
+   *
+   * @see NotFoundExceptionMapper
+   */
+  String SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION = "ERROR_IF_RESOURCE_NOT_FOUND";
+
+  /**
+   * A flag set by {@link NotFoundExceptionMapper} indicating that a 404 error response was
+   * suppressed.
+   *
+   * <p>Used primarily to allow Solr to lookup certian APIs in multiple JAX-RS applications.
+   *
+   * @see NotFoundExceptionMapper
+   */
+  String NOT_FOUND_FLAG = "RESOURCE_NOT_FOUND";
 }
diff --git a/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java b/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java
index bf09ce8d693..595027005ad 100644
--- a/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java
+++ b/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java
@@ -92,6 +92,10 @@ public class RequestMetricHandling {
     public void filter(
         ContainerRequestContext requestContext, ContainerResponseContext responseContext)
         throws IOException {
+      if (requestContext.getPropertyNames().contains(RequestContextKeys.NOT_FOUND_FLAG)) {
+        return;
+      }
+
       final RequestHandlerBase.HandlerMetrics metrics =
           (RequestHandlerBase.HandlerMetrics) requestContext.getProperty(HANDLER_METRICS);
       if (metrics == null) return;
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 4738ec46d09..8798a3c3ee7 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -72,6 +72,7 @@ import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.methods.HttpRequestBase;
 import org.apache.http.entity.InputStreamEntity;
 import org.apache.solr.api.ApiBag;
+import org.apache.solr.api.V2HttpCall;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.impl.HttpClientUtil;
 import org.apache.solr.common.SolrException;
@@ -481,6 +482,12 @@ 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 {
 
@@ -526,13 +533,14 @@ public class HttpSolrCall {
 
       HttpServletResponse resp = response;
       switch (action) {
+        case ADMIN_OR_REMOTEQUERY:
+          handleAdminOrRemoteRequest();
+          return RETURN;
         case ADMIN:
           handleAdminRequest();
           return RETURN;
         case REMOTEQUERY:
-          SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new SolrQueryResponse(), action));
-          mustClearSolrRequestInfo = true;
-          remoteQuery(coreUrl + path, resp);
+          sendRemoteQuery();
           return RETURN;
         case PROCESS:
           final Method reqMethod = Method.getMethod(req.getMethod());
@@ -602,6 +610,21 @@ public class HttpSolrCall {
     }
   }
 
+  /**
+   * Handle a request whose "type" could not be discerned in advance and may be either "admin" or
+   * "remotequery".
+   *
+   * <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
+   * handle these cases gracefully.
+   *
+   * @see V2HttpCall
+   */
+  protected void handleAdminOrRemoteRequest() throws IOException {
+    throw new IllegalStateException(
+        "handleOrForwardRequest should not be invoked when serving v1 requests.");
+  }
+
   /** Get the span for this request. Not null. */
   protected Span getSpan() {
     // Span was put into the request by SolrDispatchFilter
@@ -844,6 +867,10 @@ public class HttpSolrCall {
   private void handleAdminRequest() throws IOException {
     SolrQueryResponse solrResp = new SolrQueryResponse();
     handleAdmin(solrResp);
+    logAndFlushAdminRequest(solrResp);
+  }
+
+  protected void logAndFlushAdminRequest(SolrQueryResponse solrResp) throws IOException {
     if (solrResp.getToLog().size() > 0) {
       // has to come second and in it's own if to keep ./gradlew check happy.
       if (log.isInfoEnabled()) {
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 bc13aba4a15..97f5e254025 100644
--- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
@@ -119,7 +119,8 @@ public class SolrDispatchFilter extends BaseSolrFilter implements PathExcluder {
     RETRY,
     ADMIN,
     REMOTEQUERY,
-    PROCESS
+    PROCESS,
+    ADMIN_OR_REMOTEQUERY
   }
 
   public SolrDispatchFilter() {}
@@ -264,6 +265,7 @@ public class SolrDispatchFilter extends BaseSolrFilter implements PathExcluder {
         case ADMIN:
         case PROCESS:
         case REMOTEQUERY:
+        case ADMIN_OR_REMOTEQUERY:
         case RETURN:
           break;
       }