Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions changelog/unreleased/SOLR-16396-config-requestHandler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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
links:
- name: SOLR-16396
url: https://issues.apache.org/jira/browse/SOLR-16396
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ ConfigZnodeVersionResponse getZnodeVersion(
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does @StoreApiParameters mean?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i got introduced to this annotation when doing TaskAPI JAX-RS migrations, this annotations automatically wires the cores/collection & indexName stuffs in the v2 APIs endpoints.

Got this understanding confirmed via Cursor as well now.
Screenshot 2026-10-16 at 10 42 15 PM

@Operation(
summary = "Fetch request handlers (including implicit handler)",
tags = {"config"})
ConfigInfoResponse getRequestHandler(
@QueryParam("componentName")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is now a path param as well....

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stated the reasoning here in my self-review: #5168 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can’t put the handler name in the path the way we do for searchComponents/{name} / queryResponseWriters/{name}.

Request-handler names are paths themselves (/select, /update/json/docs, /admin/ping, …). A @PathParam would be a single segment, so names with / break routing (and encoding gets ugly).

So for request handlers we keep filtering via query param:
GET .../config/requestHandlers?componentName=/update/json/docs

@Parameter(description = "Name of the specific request handler")
String component,
Comment on lines +82 to +84

@jaykay12 jaykay12 Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the most restful we can get here.
we can't use PathParams here like we have used for /searchComponents & /queryResponseWriters.

Screenshot 2026-10-16 at 1 30 49 AM

@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)
Expand Down
12 changes: 11 additions & 1 deletion solr/api/src/java/org/apache/solr/client/api/util/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,18 @@ private Constants() {

public static final String INDEX_TYPE_PATH_PARAMETER = "indexType";
public static final String INDEX_NAME_PATH_PARAMETER = "indexName";

// "c" is accepted alongside "collections" because Solr internally forwards a v2 request to the
// node actually hosting a collection using the short "/c/{collection}/..." alias (see
// HttpSolrCall#sendRemoteProxy and V2HttpCall's "c"/"collections" handling); without it, any
// JAX-RS resource built on this prefix 404s with "Cannot find API for the path" whenever the
// request has to be proxied to another node.
public static final String INDEX_PATH_PREFIX =
"/{" + INDEX_TYPE_PATH_PARAMETER + ":cores|collections}/{" + INDEX_NAME_PATH_PARAMETER + "}";
"/{"
+ INDEX_TYPE_PATH_PARAMETER
+ ":cores|collections|c}/{"
+ INDEX_NAME_PATH_PARAMETER
+ "}";

public static final String CORE_NAME_PATH_PARAMETER = "coreName";

Expand Down
90 changes: 11 additions & 79 deletions solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
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;
Expand Down Expand Up @@ -69,7 +66,6 @@
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;
Expand Down Expand Up @@ -97,7 +93,6 @@
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;
Expand Down Expand Up @@ -221,6 +216,15 @@ private void handleGET() {
.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);
Expand All @@ -231,7 +235,8 @@ private void handleGET() {
: 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");
Expand All @@ -245,79 +250,6 @@ private void handleGET() {
}
}

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) {
Comment thread
jaykay12 marked this conversation as resolved.

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 = ApiBag.readCommands(req.getContentStreams(), resp.getValues());
if (ops == null) return;
Expand Down
75 changes: 54 additions & 21 deletions solr/core/src/java/org/apache/solr/handler/admin/api/GetConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -99,7 +101,7 @@ public GetConfig(SolrQueryRequest solrQueryRequest) {
@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;
}

Expand Down Expand Up @@ -129,13 +131,37 @@ public ConfigZnodeVersionResponse getZnodeVersion(
return response;
}

@Override
@PermissionName(CONFIG_READ_PERM)
public ConfigInfoResponse getRequestHandler(
String componentName, boolean expandParams, boolean meta) {
return getRequestHandler(REQUEST_HANDLERS, componentName, expandParams, meta);
}

public ConfigInfoResponse getRequestHandler(
String sectionName, String componentName, boolean expandParams, boolean meta) {
final var response = instantiateJerseyResponse(ConfigInfoResponse.class);

Map<String, Object> configMap = new LinkedHashMap<>();
configMap.put(
sectionName, requestHandlersWithImplicits(solrConfigAsMap(), 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);
Comment thread
jaykay12 marked this conversation as resolved.
return response;
}

Expand All @@ -144,10 +170,11 @@ public ConfigInfoResponse getConfigComponent(String component) {
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);
Comment thread
jaykay12 marked this conversation as resolved.
return response;
}

Expand All @@ -161,12 +188,12 @@ public ConfigInfoResponse getConfigComponentByName(
*/
@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;
}
Expand All @@ -183,7 +210,7 @@ public static void filterByComponentName(

Map<String, Object> pluginInfo =
o instanceof MapWriter ? new SimpleOrderedMap<>((MapWriter) o) : (Map<String, Object>) o;
section.put(
configMap.put(
component,
pluginNameVsPluginInfo instanceof MapWriter
? pluginInfo
Expand Down Expand Up @@ -227,29 +254,35 @@ private NullKeyTolerantMap jsonSafeCopy(Map<String, Object> map) {
return jsonSafeCopy;
}

@SuppressWarnings({"unchecked"})
private Map<String, Object> buildConfigMap(
SolrQueryRequest solrQueryRequest, boolean expandParams) {

private Map<String, Object> buildConfigMap(boolean expandParams) {
Map<String, Object> configMap = solrConfigAsMap();
requestHandlersWithImplicits(configMap, expandParams, null);
return jsonSafeCopy(configMap);
}

Map<String, Object> reqHandlers =
@SuppressWarnings({"unchecked"})
private Map<String, Object> requestHandlersWithImplicits(
Map<String, Object> configMap, boolean expandParams, String expandOnlyComponentName) {
Map<String, Object> reqHandlersMap =
(Map<String, Object>)
configMap.computeIfAbsent(SolrRequestHandler.TYPE, k -> new LinkedHashMap<>());

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"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ public void getSingleParamSet(SolrQueryRequest req, SolrQueryResponse rsp) {
@EndPoint(
path = {
"/config/luceneMatchVersion",
"/config/requestHandler",
"/config/initParams",
"/config/listener",
"/config/requestDispatcher",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
}
Expand Down
4 changes: 2 additions & 2 deletions solr/core/src/test/org/apache/solr/cli/PackageToolTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ void assertPackageVersion(

testForResponseElement(
cluster.getJettySolrRunner(0).getBaseUrl().toString() + "/" + collection,
"/config/requestHandler?componentName=" + component + "&meta=true",
"/config/requestHandlers?componentName=" + component + "&meta=true",
credentials,
Arrays.asList("config", "requestHandler", component, "_packageinfo_", "version"),
Arrays.asList("config", "requestHandlers", component, "_packageinfo_", "version"),
componentVersion);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void testWrongVerificationJPathIsThrown() throws IOException {
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public void populateApiBag() {
// 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");
Expand Down
Loading
Loading