Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7a77da0
cl
jaykay12 Oct 13, 2026
0db25b2
changes
jaykay12 Oct 13, 2026
79e159d
changes
jaykay12 Oct 13, 2026
da938cd
cl
jaykay12 Oct 13, 2026
32ab27a
deeper tests
jaykay12 Oct 13, 2026
1d4a3e8
Merge branch 'main' into SOLR-16396-config-query
jaykay12 Oct 14, 2026
49e6f13
changes
jaykay12 Oct 14, 2026
35f7b3d
tidy
jaykay12 Oct 14, 2026
a05946a
rebased with main
jaykay12 Oct 15, 2026
1efb602
queryparam
jaykay12 Oct 15, 2026
5baa8ab
minor
jaykay12 Oct 15, 2026
6c10a14
corrections
jaykay12 Oct 15, 2026
b02cf83
minor fix
jaykay12 Oct 15, 2026
4e159cd
added cl
jaykay12 Oct 15, 2026
05d0a9b
added tests
jaykay12 Oct 15, 2026
a7618b8
cl minor
jaykay12 Oct 16, 2026
e6b2f07
changes
jaykay12 Oct 16, 2026
061c966
fix
jaykay12 Oct 16, 2026
13dd067
fix for v1 consistency
jaykay12 Oct 16, 2026
1291fff
test fixes
jaykay12 Oct 16, 2026
3372ab7
minor cleanup
jaykay12 Oct 16, 2026
ba2c8a9
todo
jaykay12 Oct 16, 2026
e84d371
more tests coverage & strictness
jaykay12 Oct 16, 2026
6fede4c
minor
jaykay12 Oct 16, 2026
35ffae5
Merge branch 'SOLR-16396-config-requestHandler' of https://github.com…
epugh Oct 17, 2026
d444b97
Merge branch 'main' into SOLR-16396-config-requestHandler
jaykay12 Oct 17, 2026
c045334
tidying
jaykay12 Oct 17, 2026
bc37b68
Fix v2 API 404 when a /config request is proxied to another node
epugh Oct 17, 2026
5b16859
Fix ErrorProne ReferenceEquality failure in the new regression test
epugh Oct 17, 2026
1270dbf
Merge branch 'fix-v2-proxy-collection-alias' into SOLR-16396-config-r…
jaykay12 Oct 17, 2026
6f5d540
Merge branch 'main' into SOLR-16396-config-requestHandler
jaykay12 Oct 18, 2026
54d11e2
fixed packagetooltest
jaykay12 Oct 19, 2026
dc40947
cl
jaykay12 Oct 19, 2026
691e544
Merge branch 'SOLR-16396-config-requestHandler' of https://github.com…
epugh Oct 20, 2026
e0fd810
Fix a long standing bug that v1 hid but v2 stricter mapping rules on …
epugh Oct 20, 2026
cca9a44
Merge remote-tracking branch 'upstream/main' into pr/5168
epugh Oct 20, 2026
3f84d10
Simplify PackageUtils query-string parsing and GetConfig's handler-ma…
epugh Oct 20, 2026
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
12 changes: 12 additions & 0 deletions changelog/unreleased/SOLR-16396-config-requestHandler.yml
Original file line number Diff line number Diff line change
@@ -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
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
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;

Expand Down Expand Up @@ -160,10 +161,23 @@ public static String getJsonStringFromNonCollectionApi(
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);
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
85 changes: 63 additions & 22 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,41 @@ 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);
}

@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);
Comment thread
jaykay12 marked this conversation as resolved.
return response;
}

Expand All @@ -144,10 +174,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 +192,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 +214,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 +258,39 @@ private NullKeyTolerantMap jsonSafeCopy(Map<String, Object> map) {
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"})
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
Loading
Loading