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

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

commit 61208227e5d15f3268128149cfeded57089c4453
Author: Jalaz Kumar <[email protected]>
AuthorDate: Tue Oct 20 18:08:06 2026 +0530

    SOLR-16396: Migrate GET /config/requestHandler to JAX-RS (#5168)
    
    Co-authored-by: Eric Pugh <[email protected]>
    Co-authored-by: Claude Sonnet 5 <[email protected]>
    (cherry picked from commit 7e419906977def36933caf0666b6e472cf921927)
---
 .../SOLR-16396-config-requestHandler.yml           | 12 +++
 .../apache/solr/client/api/endpoint/ConfigApi.java | 21 +++++
 .../solr/cli/packagemanager/PackageUtils.java      | 16 +++-
 .../org/apache/solr/handler/SolrConfigHandler.java | 90 +++-----------------
 .../apache/solr/handler/admin/api/GetConfig.java   | 85 ++++++++++++++-----
 .../solr/handler/admin/api/GetConfigAPI.java       |  1 -
 .../question-answer-repository/repository.json     | 33 +++++++-
 .../cli/packagemanager/TestPackageManager.java     |  2 +-
 .../solr/handler/admin/V2ConfigAPIMappingTest.java |  1 -
 .../handler/admin/api/GetConfigSolrCloudTest.java  | 95 ++++++++++++++++++++++
 .../solr/handler/admin/api/GetConfigTest.java      | 89 ++++++++++++++++++++
 .../configuration-guide/pages/config-api.adoc      |  8 +-
 .../pages/implicit-requesthandlers.adoc            |  4 +-
 13 files changed, 343 insertions(+), 114 deletions(-)

diff --git a/changelog/unreleased/SOLR-16396-config-requestHandler.yml 
b/changelog/unreleased/SOLR-16396-config-requestHandler.yml
new file mode 100644
index 00000000000..44164b792c5
--- /dev/null
+++ b/changelog/unreleased/SOLR-16396-config-requestHandler.yml
@@ -0,0 +1,12 @@
+title: >
+  Migrate GET /config/requestHandler to JAX-RS; V2 path and response key
+  are now plural requestHandlers (V1 unchanged)
+type: changed
+authors:
+  - name: Jalaz Kumar
+    nick: jaykay12
+  - name: Eric Pugh
+    nick: epugh
+links:
+  - name: SOLR-16396
+    url: https://issues.apache.org/jira/browse/SOLR-16396
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
index 49a4b018cc9..f10026be924 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigApi.java
@@ -70,6 +70,27 @@ public interface ConfigApi {
             @Parameter(description = "Expected request-params znode version")
             int expectedParamsVersion);
 
+    // TODO: RH names contain '/' (e.g. /select, /update/json), so they cannot 
be a @PathParam in
+    // the current design; Keeping filtering via @QueryParam for now.
+    @GET
+    @Path("/requestHandlers")
+    @StoreApiParameters
+    @Operation(
+        summary = "Fetch request handlers (including implicit handler)",
+        tags = {"config"})
+    ConfigInfoResponse getRequestHandler(
+        @QueryParam("componentName")
+            @Parameter(description = "Name of the specific request handler")
+            String component,
+        @QueryParam("expandParams")
+            @DefaultValue("false")
+            @Parameter(description = "Expand request handler useParams and 
effective parameters")
+            boolean expandParams,
+        @QueryParam("meta")
+            @DefaultValue("false")
+            @Parameter(description = "Include package metadata 
(_packageinfo_)")
+            boolean meta);
+
     // TODO: Keep this regex in sync with GetConfig.MIGRATED_CONFIG_COMPONENTS 
("searchComponents"
     // and "queryResponseWriters" here are the external, pluralized names for 
the internal
     // "searchComponent"/"queryResponseWriter" sections; see 
GetConfig#toExternalComponentName)
diff --git 
a/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java 
b/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java
index 35fdc24d97d..72d76ddf844 100644
--- a/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java
+++ b/solr/core/src/java/org/apache/solr/cli/packagemanager/PackageUtils.java
@@ -51,6 +51,7 @@ import org.apache.solr.common.util.Utils;
 import org.apache.solr.filestore.ClusterFileStore;
 import org.apache.solr.filestore.DistribFileStore;
 import org.apache.solr.filestore.FileStoreAPI;
+import org.apache.solr.servlet.SolrRequestParsers;
 import org.apache.solr.util.SolrJacksonAnnotationInspector;
 import org.apache.zookeeper.server.ByteBufferInputStream;
 
@@ -160,10 +161,23 @@ public class PackageUtils {
     return getJsonStringFromUrl(client, path, params, false);
   }
 
-  /** Returns JSON string from a given Solr URL */
+  /**
+   * repository.json's verify/setup command templates embed their query string 
directly in {@code
+   * path} (e.g. {@code .../requestHandlers?componentName=x&meta=true}). The 
HTTP client always
+   * appends its own query string afterward with a leading {@code ?}, and a 
URL with two {@code ?}
+   * characters is parsed as a single query string on the wire -- so the last 
templated parameter
+   * silently absorbs the client's params as part of its own value instead of 
being read correctly.
+   */
   private static String getJsonStringFromUrl(
       SolrClient client, String path, SolrParams params, boolean 
isCollectionApi) {
     try {
+      int queryIndex = path.indexOf('?');
+      if (queryIndex >= 0) {
+        ModifiableSolrParams merged = new ModifiableSolrParams(params);
+        
merged.add(SolrRequestParsers.parseQueryString(path.substring(queryIndex + 1)));
+        params = merged;
+        path = path.substring(0, queryIndex);
+      }
       GenericSolrRequest request =
           new GenericSolrRequest(SolrRequest.METHOD.GET, path, params)
               .setRequiresCollection(isCollectionApi);
diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java 
b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index b4d8a5eee78..1f9fe5f1e94 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -21,10 +21,7 @@ import static 
org.apache.solr.common.util.StrUtils.formatString;
 import static org.apache.solr.core.ConfigOverlay.NOT_EDITABLE;
 import static org.apache.solr.core.ConfigOverlay.ZNODEVER;
 import static org.apache.solr.core.ConfigSetProperties.IMMUTABLE_CONFIGSET_ARG;
-import static org.apache.solr.core.PluginInfo.APPENDS;
 import static org.apache.solr.core.PluginInfo.DEFAULTS;
-import static org.apache.solr.core.PluginInfo.INVARIANTS;
-import static org.apache.solr.core.RequestParams.USEPARAM;
 import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_CLASS;
 import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME;
 import static 
org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME_IN_OVERLAY;
@@ -68,7 +65,6 @@ import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.CommandOperation;
@@ -96,7 +92,6 @@ import org.apache.solr.schema.SchemaManager;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
 import org.apache.solr.util.RTimer;
-import org.apache.solr.util.SolrPluginUtils;
 import org.apache.solr.util.plugin.SolrCoreAware;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -220,6 +215,15 @@ public class SolrConfigHandler extends RequestHandlerBase
                     .getZnodeVersion(
                         req.getParams().getInt(ConfigOverlay.NAME, -1),
                         req.getParams().getInt(RequestParams.NAME, -1)));
+          } else if (SolrRequestHandler.TYPE.equals(parts.get(1))) {
+            V2ApiUtils.squashIntoSolrResponseWithoutHeader(
+                resp,
+                new GetConfig(req)
+                    .getRequestHandler(
+                        parts.get(1),
+                        req.getParams().get("componentName"),
+                        req.getParams().getBool("expandParams", false),
+                        req.getParams().getBool("meta", false)));
           } else if 
(GetConfig.MIGRATED_CONFIG_COMPONENTS.contains(parts.get(1))) {
             String componentName = req.getParams().get("componentName");
             GetConfig getConfig = new GetConfig(req);
@@ -230,7 +234,8 @@ public class SolrConfigHandler extends RequestHandlerBase
                     : getConfig.getConfigComponentByName(
                         parts.get(1), componentName, 
req.getParams().getBool("meta", false)));
           } else {
-            Map<String, Object> m = getConfigDetails(parts.get(1), req);
+            Map<String, Object> m =
+                Utils.convertToMap(req.getCore().getSolrConfig(), new 
LinkedHashMap<>());
             Map<String, Object> val = new LinkedHashMap<>();
             val.put(parts.get(1), m.get(parts.get(1)));
             String componentName = req.getParams().get("componentName");
@@ -244,79 +249,6 @@ public class SolrConfigHandler extends RequestHandlerBase
       }
     }
 
-    private Map<String, Object> getConfigDetails(String componentType, 
SolrQueryRequest req) {
-      String componentName = componentType == null ? null : 
req.getParams().get("componentName");
-      boolean showParams = req.getParams().getBool("expandParams", false);
-      Map<String, Object> map =
-          Utils.convertToMap(this.req.getCore().getSolrConfig(), new 
LinkedHashMap<>());
-      if (componentType != null && 
!SolrRequestHandler.TYPE.equals(componentType)) return map;
-
-      @SuppressWarnings({"unchecked"})
-      Map<String, Object> reqHandlers =
-          (Map<String, Object>)
-              map.computeIfAbsent(SolrRequestHandler.TYPE, k -> new 
LinkedHashMap<>());
-      List<PluginInfo> plugins = this.req.getCore().getImplicitHandlers();
-      for (PluginInfo plugin : plugins) {
-        if (SolrRequestHandler.TYPE.equals(plugin.type)) {
-          if (!reqHandlers.containsKey(plugin.name)) {
-            reqHandlers.put(plugin.name, plugin);
-          }
-        }
-      }
-      if (!showParams) return map;
-      for (Map.Entry<String, Object> e : reqHandlers.entrySet()) {
-        if (componentName == null || e.getKey().equals(componentName)) {
-          Map<String, Object> m = expandUseParams(req, e.getValue());
-          e.setValue(m);
-        }
-      }
-
-      return map;
-    }
-
-    @SuppressWarnings({"unchecked"})
-    private Map<String, Object> expandUseParams(SolrQueryRequest req, Object 
plugin) {
-
-      Map<String, Object> pluginInfo = null;
-      if (plugin instanceof Map) {
-        pluginInfo = (Map) plugin;
-      } else if (plugin instanceof PluginInfo) {
-        pluginInfo = Utils.convertToMap((PluginInfo) plugin, new 
LinkedHashMap<>());
-      }
-      String useParams = (String) pluginInfo.get(USEPARAM);
-      String useParamsInReq = req.getOriginalParams().get(USEPARAM);
-      if (useParams != null || useParamsInReq != null) {
-        Map<String, Object> m = new LinkedHashMap<>();
-        pluginInfo.put("_useParamsExpanded_", m);
-        List<String> params = new ArrayList<>();
-        if (useParams != null) params.addAll(StrUtils.splitSmart(useParams, 
','));
-        if (useParamsInReq != null) 
params.addAll(StrUtils.splitSmart(useParamsInReq, ','));
-        for (String param : params) {
-          RequestParams.ParamSet p =
-              
this.req.getCore().getSolrConfig().getRequestParams().getParams(param);
-          if (p != null) {
-            m.put(param, p);
-          } else {
-            m.put(param, "[NOT AVAILABLE]");
-          }
-        }
-
-        SolrQueryRequest r = req.subRequest(req.getOriginalParams());
-        r.getContext().put(USEPARAM, useParams);
-        NamedList<?> nl = new PluginInfo(SolrRequestHandler.TYPE, 
pluginInfo).initArgs;
-        SolrPluginUtils.setDefaults(
-            r,
-            getSolrParamsFromNamedList(nl, DEFAULTS),
-            getSolrParamsFromNamedList(nl, APPENDS),
-            getSolrParamsFromNamedList(nl, INVARIANTS));
-        // SolrParams.wrapDefaults(maskUseParams, req.getParams())
-
-        MapSolrParams mask = new MapSolrParams(Map.of("componentName", "", 
"expandParams", ""));
-        pluginInfo.put("_effectiveParams_", SolrParams.wrapDefaults(mask, 
r.getParams()));
-      }
-      return pluginInfo;
-    }
-
     private void handlePOST() throws IOException {
       List<CommandOperation> ops =
           CommandOperation.readCommands(req.getContentStreams(), 
resp.getValues());
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
index 1bc7db362bd..b8efea0a11c 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
@@ -68,6 +68,8 @@ public class GetConfig extends JerseyResource implements 
ConfigApi.Get {
           "searchComponent",
           "queryResponseWriter");
 
+  public static final String REQUEST_HANDLERS = "requestHandlers";
+
   // v2-only renames: external (URL/response-key) component name -> internal 
SolrConfig section
   // name. v1's GET /config/{component} keeps using the internal names 
unchanged.
   private static final Map<String, String> EXTERNAL_TO_INTERNAL_COMPONENT_NAME 
=
@@ -99,7 +101,7 @@ public class GetConfig extends JerseyResource implements 
ConfigApi.Get {
   @PermissionName(CONFIG_READ_PERM)
   public ConfigInfoResponse getConfig(boolean expandParams) {
     final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
-    response.config = buildConfigMap(solrQueryRequest, expandParams);
+    response.config = buildConfigMap(expandParams);
     return response;
   }
 
@@ -129,13 +131,41 @@ public class GetConfig extends JerseyResource implements 
ConfigApi.Get {
     return response;
   }
 
+  @Override
+  @PermissionName(CONFIG_READ_PERM)
+  public ConfigInfoResponse getRequestHandler(
+      String componentName, boolean expandParams, boolean meta) {
+    return getRequestHandler(REQUEST_HANDLERS, componentName, expandParams, 
meta);
+  }
+
+  @SuppressWarnings("unchecked")
+  public ConfigInfoResponse getRequestHandler(
+      String sectionName, String componentName, boolean expandParams, boolean 
meta) {
+    final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
+
+    Map<String, Object> reqHandlersMap =
+        (Map<String, Object>)
+            solrConfigAsMap().computeIfAbsent(SolrRequestHandler.TYPE, k -> 
new LinkedHashMap<>());
+    Map<String, Object> configMap = new LinkedHashMap<>();
+    configMap.put(
+        sectionName, requestHandlersWithImplicits(reqHandlersMap, 
expandParams, componentName));
+    if (componentName != null) {
+      filterByComponentName(configMap, sectionName, componentName, meta, 
solrQueryRequest);
+    }
+
+    response.config = jsonSafeCopy(configMap);
+    return response;
+  }
+
   @Override
   @PermissionName(CONFIG_READ_PERM)
   public ConfigInfoResponse getConfigComponent(String component) {
     final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
-    Map<String, Object> section = new LinkedHashMap<>();
-    section.put(component, 
solrConfigAsMap().get(toInternalComponentName(component)));
-    response.config = jsonSafeCopy(section);
+
+    Map<String, Object> configMap = new LinkedHashMap<>();
+    configMap.put(component, 
solrConfigAsMap().get(toInternalComponentName(component)));
+
+    response.config = jsonSafeCopy(configMap);
     return response;
   }
 
@@ -144,10 +174,11 @@ public class GetConfig extends JerseyResource implements 
ConfigApi.Get {
   public ConfigInfoResponse getConfigComponentByName(
       String component, String componentName, boolean meta) {
     final var response = instantiateJerseyResponse(ConfigInfoResponse.class);
-    Map<String, Object> section = new LinkedHashMap<>();
-    section.put(component, 
solrConfigAsMap().get(toInternalComponentName(component)));
-    filterByComponentName(section, component, componentName, meta, 
solrQueryRequest);
-    response.config = jsonSafeCopy(section);
+
+    Map<String, Object> configMap = new LinkedHashMap<>();
+    configMap.put(component, 
solrConfigAsMap().get(toInternalComponentName(component)));
+    filterByComponentName(configMap, component, componentName, meta, 
solrQueryRequest);
+    response.config = jsonSafeCopy(configMap);
     return response;
   }
 
@@ -161,12 +192,12 @@ public class GetConfig extends JerseyResource implements 
ConfigApi.Get {
    */
   @SuppressWarnings({"unchecked", "rawtypes"})
   public static void filterByComponentName(
-      Map<String, Object> section,
+      Map<String, Object> configMap,
       String component,
       String componentName,
       boolean meta,
       SolrQueryRequest req) {
-    Map pluginNameVsPluginInfo = (Map) section.get(component);
+    Map pluginNameVsPluginInfo = (Map) configMap.get(component);
     if (pluginNameVsPluginInfo == null) {
       return;
     }
@@ -183,7 +214,7 @@ public class GetConfig extends JerseyResource implements 
ConfigApi.Get {
 
     Map<String, Object> pluginInfo =
         o instanceof MapWriter ? new SimpleOrderedMap<>((MapWriter) o) : 
(Map<String, Object>) o;
-    section.put(
+    configMap.put(
         component,
         pluginNameVsPluginInfo instanceof MapWriter
             ? pluginInfo
@@ -227,29 +258,39 @@ public class GetConfig extends JerseyResource implements 
ConfigApi.Get {
     return jsonSafeCopy;
   }
 
-  @SuppressWarnings({"unchecked"})
-  private Map<String, Object> buildConfigMap(
-      SolrQueryRequest solrQueryRequest, boolean expandParams) {
-
+  @SuppressWarnings("unchecked")
+  private Map<String, Object> buildConfigMap(boolean expandParams) {
     Map<String, Object> configMap = solrConfigAsMap();
-
-    Map<String, Object> reqHandlers =
+    Map<String, Object> reqHandlersMap =
         (Map<String, Object>)
             configMap.computeIfAbsent(SolrRequestHandler.TYPE, k -> new 
LinkedHashMap<>());
+    requestHandlersWithImplicits(reqHandlersMap, expandParams, null);
+    return jsonSafeCopy(configMap);
+  }
+
+  /**
+   * Merges implicit handlers into {@code reqHandlersMap} (mutated in place 
and returned), expanding
+   * useParams for {@code expandOnlyComponentName} (or all entries if null) 
when {@code
+   * expandParams}.
+   */
+  private Map<String, Object> requestHandlersWithImplicits(
+      Map<String, Object> reqHandlersMap, boolean expandParams, String 
expandOnlyComponentName) {
     List<PluginInfo> plugins = 
solrQueryRequest.getCore().getImplicitHandlers();
     for (PluginInfo plugin : plugins) {
-      if (SolrRequestHandler.TYPE.equals(plugin.type) && 
!reqHandlers.containsKey(plugin.name)) {
-        reqHandlers.put(plugin.name, plugin);
+      if (SolrRequestHandler.TYPE.equals(plugin.type) && 
!reqHandlersMap.containsKey(plugin.name)) {
+        reqHandlersMap.put(plugin.name, plugin);
       }
     }
 
     if (expandParams) {
-      for (Map.Entry<String, Object> entry : reqHandlers.entrySet()) {
-        entry.setValue(expandUseParams(solrQueryRequest, entry.getValue()));
+      for (Map.Entry<String, Object> entry : reqHandlersMap.entrySet()) {
+        if (expandOnlyComponentName == null || 
expandOnlyComponentName.equals(entry.getKey())) {
+          entry.setValue(expandUseParams(solrQueryRequest, entry.getValue()));
+        }
       }
     }
 
-    return jsonSafeCopy(configMap);
+    return reqHandlersMap;
   }
 
   @SuppressWarnings({"unchecked", "rawtypes"})
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
index 808bdf545f4..6cd43791100 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
@@ -61,7 +61,6 @@ public class GetConfigAPI {
   @EndPoint(
       path = {
         "/config/luceneMatchVersion",
-        "/config/requestHandler",
         "/config/initParams",
         "/config/listener",
         "/config/requestDispatcher",
diff --git 
a/solr/core/src/test-files/solr/question-answer-repository/repository.json 
b/solr/core/src/test-files/solr/question-answer-repository/repository.json
index 05fad905978..626361d25d5 100644
--- a/solr/core/src/test-files/solr/question-answer-repository/repository.json
+++ b/solr/core/src/test-files/solr/question-answer-repository/repository.json
@@ -28,9 +28,9 @@
                 "method": "POST"
               },
               "verify-command": {
-                "path": 
"/api/collections/${collection}/config/requestHandler?componentName=${RH-HANDLER-PATH}&meta=true",
+                "path": 
"/api/collections/${collection}/config/requestHandlers?componentName=${RH-HANDLER-PATH}&meta=true",
                 "method": "GET",
-                "condition": 
"$['config'].['requestHandler'].['${RH-HANDLER-PATH}'].['_packageinfo_'].['version']",
+                "condition": 
"$['config'].['requestHandlers'].['${RH-HANDLER-PATH}'].['_packageinfo_'].['version']",
                 "expected": "${package-version}"
               }
             }
@@ -48,7 +48,34 @@
             "url": "question-answer-request-handler-1.1.jar.tmp",
             "sig": 
"2PBHV2xBqd+teFpSY322lviCrr6EXB7g1Q7UAZcNsr2qzR+w+sPSyfV4YYeHKHYbMkyw1YBz11Gw/Fdgd1VIOQ=="
           }
-        ]
+        ],
+        "manifest": {
+          "version-constraint": ">8",
+          "plugins": [
+            {
+              "name": "request-handler",
+              "setup-command": {
+                "path": "/api/collections/${collection}/config",
+                "payload": {"add-requesthandler": {"name": 
"${RH-HANDLER-PATH}", "class": "question-answer:fullstory.QARequestHandler"}},
+                "method": "POST"
+              },
+              "uninstall-command": {
+                "path": "/api/collections/${collection}/config",
+                "payload": {"delete-requesthandler": "${RH-HANDLER-PATH}"},
+                "method": "POST"
+              },
+              "verify-command": {
+                "path": 
"/api/collections/${collection}/config/requestHandlers?componentName=${RH-HANDLER-PATH}&meta=true",
+                "method": "GET",
+                "condition": 
"$['config'].['requestHandlers'].['${RH-HANDLER-PATH}'].['_packageinfo_'].['version']",
+                "expected": "${package-version}"
+              }
+            }
+          ],
+          "parameter-defaults": {
+            "RH-HANDLER-PATH": "/mypath"
+          }
+        }
       }
     ]
   }
diff --git 
a/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java 
b/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java
index 2793af1641d..825bd3b5a3c 100644
--- 
a/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java
+++ 
b/solr/core/src/test/org/apache/solr/cli/packagemanager/TestPackageManager.java
@@ -60,7 +60,7 @@ public class TestPackageManager extends SolrCloudTestCase {
         plugin.verifyCommand = new SolrPackage.Command();
         plugin.verifyCommand.method = "GET";
         plugin.verifyCommand.path = /*"/api*/
-            "/collections/" + COLLECTION_NAME + "/config/requestHandler";
+            "/collections/" + COLLECTION_NAME + "/config/requestHandlers";
         plugin.verifyCommand.condition = "&[no_quotes_error_jpath]";
         manager.verify(
             new SolrPackageInstance(
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
index 6e0a2aa6e27..e7912afa8fa 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
@@ -49,7 +49,6 @@ public class V2ConfigAPIMappingTest extends 
V2ApiMappingTest<SolrConfigHandler>
   // the API lookup works for a handful of the valid config "components".
   @Test
   public void testGetSingleComponentConfig() {
-    assertAnnotatedApiExistsFor("GET", "/config/requestHandler");
     assertAnnotatedApiExistsFor("GET", "/config/jmx");
     assertAnnotatedApiExistsFor("GET", "/config/requestDispatcher");
     assertAnnotatedApiExistsFor("GET", "/config/luceneMatchVersion");
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
index dd51d21ddea..20dbd09d1a6 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java
@@ -17,8 +17,11 @@
 package org.apache.solr.handler.admin.api;
 
 import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+import static org.apache.solr.handler.admin.api.GetConfig.REQUEST_HANDLERS;
 
+import java.util.Map;
 import org.apache.solr.client.api.model.IndexType;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.ConfigApi;
@@ -82,6 +85,12 @@ public class GetConfigSolrCloudTest extends 
SolrCloudTestCase {
     assertTrue(response.znodeVersion.params >= -1);
   }
 
+  /**
+   * Reproduces a bug where a v2 "/config" request proxied internally to the 
node actually hosting
+   * the collection fails with "Cannot find API for the path", because Solr 
forwards such requests
+   * using the short "/c/{collection}/..." alias, but {@code 
Constants.INDEX_PATH_PREFIX}'s JAX-RS
+   * {@code @Path} regex only recognizes "cores" or "collections", not "c".
+   */
   @Test
   public void testGetOverlayFromCollectionViaNonHostingNode() throws Exception 
{
     Replica replica = 
getCollectionState(COLLECTION_NAME).replicaStream().findFirst().orElseThrow();
@@ -103,6 +112,92 @@ public class GetConfigSolrCloudTest extends 
SolrCloudTestCase {
     }
   }
 
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersFromCollection() throws Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION, 
COLLECTION_NAME);
+    var response = request.process(cluster.getSolrClient());
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertNotNull(response.config);
+    assertEquals(1, response.config.size());
+    assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+    assertNotNull(configSection);
+    assertTrue(configSection.containsKey("/select"));
+    assertTrue(configSection.containsKey("/admin/ping"));
+    assertTrue(configSection.containsKey("/graph"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersFilteredByNameFromCollection() throws 
Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION, 
COLLECTION_NAME);
+    request.setComponentName("/select");
+    var response = request.process(cluster.getSolrClient());
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertEquals(1, response.config.size());
+    assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+    assertNotNull(configSection);
+    assertEquals(1, configSection.size());
+    assertTrue(configSection.containsKey("/select"));
+
+    Map<String, Object> selectRHSection = (Map<String, Object>) 
configSection.get("/select");
+    assertEquals("/select", selectRHSection.get("name"));
+    assertEquals("solr.SearchHandler", selectRHSection.get("class"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersExpandParamsTrue() throws Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION, 
COLLECTION_NAME);
+    request.setComponentName("/admin/ping");
+    request.setExpandParams(true);
+    var response = request.process(cluster.getSolrClient());
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertEquals(1, response.config.size());
+    assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+
+    Map<String, Object> pingRHSection = (Map<String, Object>) 
configSection.get("/admin/ping");
+    assertTrue(pingRHSection.containsKey("_useParamsExpanded_"));
+    assertNotNull(pingRHSection.get("_useParamsExpanded_"));
+    Map<String, Object> expandedPingRHSection =
+        (Map<String, Object>) pingRHSection.get("_useParamsExpanded_");
+    assertEquals("[NOT AVAILABLE]", expandedPingRHSection.get("_ADMIN_PING"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersOmitsExpandedParamsByDefault() throws 
Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION, 
COLLECTION_NAME);
+    request.setComponentName("/admin/ping");
+    var response = request.process(cluster.getSolrClient());
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+
+    Map<String, Object> pingRHSection = (Map<String, Object>) 
configSection.get("/admin/ping");
+    assertFalse(pingRHSection.containsKey("_useParamsExpanded_"));
+  }
+
+  @Test
+  public void testGetRequestHandlersByUnknownNameReturnsNotFound() throws 
Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.COLLECTION, 
COLLECTION_NAME);
+    request.setComponentName("/randomHandler");
+    final RemoteSolrException exception =
+        assertThrows(RemoteSolrException.class, () -> 
request.process(cluster.getSolrClient()));
+    assertEquals(404, exception.code());
+  }
+
   @Test
   public void testGetConfigComponentFromCollection() throws Exception {
     for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
index 504fe7ecb5e..4407845d155 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigTest.java
@@ -17,6 +17,7 @@
 package org.apache.solr.handler.admin.api;
 
 import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+import static org.apache.solr.handler.admin.api.GetConfig.REQUEST_HANDLERS;
 
 import java.util.Map;
 import org.apache.solr.SolrTestCase;
@@ -83,6 +84,94 @@ public class GetConfigTest extends SolrTestCase {
     assertTrue(response.znodeVersion.params >= -1);
   }
 
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersFromCore() throws Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+    var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertNotNull(response.config);
+    assertEquals(1, response.config.size());
+    assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+    assertNotNull(configSection);
+    assertTrue(configSection.containsKey("/select"));
+    assertTrue(configSection.containsKey("/admin/ping"));
+    assertTrue(configSection.containsKey("/spell"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersFilteredByNameFromCore() throws Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+    request.setComponentName("/select");
+    var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertEquals(1, response.config.size());
+    assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+    assertNotNull(configSection);
+    assertEquals(1, configSection.size());
+    assertTrue(configSection.containsKey("/select"));
+
+    Map<String, Object> selectRHSection = (Map<String, Object>) 
configSection.get("/select");
+    assertEquals("/select", selectRHSection.get("name"));
+    assertEquals("solr.SearchHandler", selectRHSection.get("class"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersExpandParamsTrue() throws Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+    request.setComponentName("/admin/ping");
+    request.setExpandParams(true);
+    var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+    assertNotNull(response);
+    assertNull(response.error);
+    assertEquals(1, response.config.size());
+    assertTrue(response.config.containsKey(REQUEST_HANDLERS));
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+
+    Map<String, Object> pingRHSection = (Map<String, Object>) 
configSection.get("/admin/ping");
+    assertTrue(pingRHSection.containsKey("_useParamsExpanded_"));
+    assertNotNull(pingRHSection.get("_useParamsExpanded_"));
+    Map<String, Object> expandedPingRHSection =
+        (Map<String, Object>) pingRHSection.get("_useParamsExpanded_");
+    assertEquals("[NOT AVAILABLE]", expandedPingRHSection.get("_ADMIN_PING"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testGetRequestHandlersOmitsExpandedParamsByDefault() throws 
Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+    request.setComponentName("/admin/ping");
+    var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+    Map<String, Object> configSection = (Map<String, Object>) 
response.config.get(REQUEST_HANDLERS);
+
+    Map<String, Object> pingRHSection = (Map<String, Object>) 
configSection.get("/admin/ping");
+    assertFalse(pingRHSection.containsKey("_useParamsExpanded_"));
+  }
+
+  @Test
+  public void testGetRequestHandlersByUnknownNameReturnsNotFound() throws 
Exception {
+    var request = new ConfigApi.GetRequestHandler(IndexType.CORE, CORE_NAME);
+    request.setComponentName("/randomHandler");
+    final RemoteSolrException exception =
+        assertThrows(
+            RemoteSolrException.class,
+            () -> request.process(solrTestRule.getSolrClient(CORE_NAME)));
+    assertEquals(404, exception.code());
+  }
+
   @Test
   public void testGetConfigComponentFromCore() throws Exception {
     for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) {
diff --git 
a/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc 
b/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc
index 38981e7caf6..01c043ae6a6 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/config-api.adoc
@@ -109,7 +109,7 @@ V2 API::
 ====
 [source,bash]
 ----
-http://localhost:8983/api/collections/techproducts/config/requestHandler
+http://localhost:8983/api/collections/techproducts/config/requestHandlers
 ----
 ====
 ======
@@ -119,7 +119,7 @@ To see the configuration for implicit request handlers, add 
`expandParams=true`
 See the documentation for implicit request handlers linked above for examples 
using this command.
 
 The available top-level sections that can be added as path parameters are: 
`query`, `requestHandler`, `searchComponent`, `updateHandler`, 
`queryResponseWriter`, `initParams`, `znodeVersion`, `listener`, 
`directoryFactory`, `indexConfig`, and `codecFactory`.
-In the V2 API, `searchComponent` and `queryResponseWriter` are exposed under 
the pluralized names `searchComponents` and `queryResponseWriters` instead.
+In the V2 API, `requestHandler`, `searchComponent` and `queryResponseWriter` 
are exposed under the pluralized names `requestHandlers`, `searchComponents` 
and `queryResponseWriters` instead (including the response JSON key).
 
 To further restrict the request to a single component within a top-level 
section, use the `componentName` request parameter.
 
@@ -140,12 +140,12 @@ V2 API::
 ====
 [source,bash]
 ----
-http://localhost:8983/api/collections/techproducts/config/requestHandler?componentName=/select
+http://localhost:8983/api/collections/techproducts/config/requestHandlers?componentName=/select
 ----
 ====
 ======
 
-The output of this command will look similar to:
+The output of this command will look similar to(V1 nests under 
`requestHandler`; V2 under `requestHandlers`):
 
 [source,json]
 ----
diff --git 
a/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
 
b/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
index 0aad010ddc9..67948d8f144 100644
--- 
a/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
+++ 
b/solr/solr-ref-guide/modules/configuration-guide/pages/implicit-requesthandlers.adoc
@@ -693,12 +693,12 @@ V2 API::
 ====
 [source,bash]
 ----
-http://localhost:8983/api/collections/gettingstarted/config/requestHandler?componentName=/export&expandParams=true
+http://localhost:8983/api/collections/gettingstarted/config/requestHandlers?componentName=/export&expandParams=true
 ----
 ====
 ======
 
-The response will look similar to:
+The response will look similar to (V1 nests under `requestHandler`; V2 nests 
under `requestHandlers`):
 
 [source,json]
 ----

Reply via email to