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

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


The following commit(s) were added to refs/heads/main by this push:
     new abea3b759c7 Separate core specific Request Writers from node specific 
"built in" ones.  Move core specific to using ImplicitPlugins.json. (#4073)
abea3b759c7 is described below

commit abea3b759c7c0be88f01b946c16378ae5d68f0dc
Author: Eric Pugh <[email protected]>
AuthorDate: Sat Jan 31 10:05:26 2026 -0500

    Separate core specific Request Writers from node specific "built in" ones.  
Move core specific to using ImplicitPlugins.json. (#4073)
    
    This PR moves core-specific QueryResponseWriter defaults out of hardcoded 
Java and into ImplicitPlugins.json config file, while introducing a minimal 
built-in response writer set for admin/container-level requests that have no 
SolrCore via ResponseWritersRegistery class.   We also also introduce a 
FileStreamResponseWriter that replaces a anonymous class that required special 
logic to create.
---
 .../admin-response-writers-minimal-set.yml         |   9 ++
 .../solr/bench/search/QueryResponseWriters.java    |   2 -
 .../src/java/org/apache/solr/core/SolrCore.java    | 146 ++++++++++----------
 .../solr/handler/admin/SystemInfoHandler.java      |   7 +-
 .../org/apache/solr/request/SolrQueryRequest.java  |  16 ++-
 .../solr/response/FileStreamResponseWriter.java    |  67 +++++++++
 .../solr/response/ResponseWritersRegistry.java     |  93 +++++++++++++
 .../apache/solr/response/SolrQueryResponse.java    |   4 +-
 .../java/org/apache/solr/servlet/HttpSolrCall.java |   4 +-
 solr/core/src/resources/ImplicitPlugins.json       |  20 +++
 .../org/apache/solr/core/TestImplicitPlugins.java  |  78 +++++++++++
 .../response/TestFileStreamResponseWriter.java     | 149 +++++++++++++++++++++
 .../solr/response/TestResponseWritersRegistry.java |  64 +++++++++
 13 files changed, 570 insertions(+), 89 deletions(-)

diff --git a/changelog/unreleased/admin-response-writers-minimal-set.yml 
b/changelog/unreleased/admin-response-writers-minimal-set.yml
new file mode 100644
index 00000000000..64b59f93ab6
--- /dev/null
+++ b/changelog/unreleased/admin-response-writers-minimal-set.yml
@@ -0,0 +1,9 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Introduce minimal set of request writers for node/container-level 
requests. Core-specific request writers now leverage ImplicitPlugins.json for 
creation.
+type: other
+authors:
+  - name: Eric Pugh
+  - name: David Smiley
+links:
+- name: PR#4073
+  url: https://github.com/apache/solr/pull/4073
diff --git 
a/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
 
b/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
index 67d931c9217..4b6118fed27 100644
--- 
a/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
+++ 
b/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
@@ -30,7 +30,6 @@ import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.core.SolrCore;
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.BenchmarkMode;
 import org.openjdk.jmh.annotations.Fork;
@@ -58,7 +57,6 @@ public class QueryResponseWriters {
   @State(Scope.Benchmark)
   public static class BenchState {
 
-    /** See {@link SolrCore#DEFAULT_RESPONSE_WRITERS} */
     @Param({CommonParams.JAVABIN, CommonParams.JSON, "cbor", "smile", "xml", 
"raw"})
     String wt;
 
diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java 
b/solr/core/src/java/org/apache/solr/core/SolrCore.java
index 4e73bd530f3..c3aac28380e 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -17,8 +17,6 @@
 package org.apache.solr.core;
 
 import static org.apache.solr.common.params.CommonParams.PATH;
-import static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT;
-import static 
org.apache.solr.handler.admin.MetricsHandler.PROMETHEUS_METRICS_WT;
 import static org.apache.solr.metrics.SolrCoreMetricManager.COLLECTION_ATTR;
 import static org.apache.solr.metrics.SolrCoreMetricManager.CORE_ATTR;
 import static org.apache.solr.metrics.SolrCoreMetricManager.REPLICA_TYPE_ATTR;
@@ -112,7 +110,6 @@ import 
org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager.SnapshotMetaDa
 import org.apache.solr.handler.IndexFetcher;
 import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.handler.SolrConfigHandler;
-import org.apache.solr.handler.admin.api.ReplicationAPIBase;
 import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.handler.component.HighlightComponent;
 import org.apache.solr.handler.component.SearchComponent;
@@ -129,19 +126,9 @@ import org.apache.solr.pkg.SolrPackageLoader;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.request.SolrRequestInfo;
-import org.apache.solr.response.CSVResponseWriter;
-import org.apache.solr.response.CborResponseWriter;
-import org.apache.solr.response.GeoJSONResponseWriter;
-import org.apache.solr.response.GraphMLResponseWriter;
-import org.apache.solr.response.JacksonJsonWriter;
-import org.apache.solr.response.JavaBinResponseWriter;
-import org.apache.solr.response.PrometheusResponseWriter;
 import org.apache.solr.response.QueryResponseWriter;
-import org.apache.solr.response.RawResponseWriter;
-import org.apache.solr.response.SchemaXmlResponseWriter;
-import org.apache.solr.response.SmileResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
 import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.response.XMLResponseWriter;
 import org.apache.solr.response.transform.TransformerFactory;
 import org.apache.solr.rest.ManagedResourceStorage;
 import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
@@ -3088,51 +3075,6 @@ public class SolrCore implements SolrInfoBean, Closeable 
{
 
   private final PluginBag<QueryResponseWriter> responseWriters =
       new PluginBag<>(QueryResponseWriter.class, this);
-  public static final Map<String, QueryResponseWriter> 
DEFAULT_RESPONSE_WRITERS;
-
-  static {
-    HashMap<String, QueryResponseWriter> m = new HashMap<>(15, 1);
-    m.put("xml", new XMLResponseWriter());
-    m.put(CommonParams.JSON, new JacksonJsonWriter());
-    m.put("standard", m.get(CommonParams.JSON));
-    m.put("geojson", new GeoJSONResponseWriter());
-    m.put("graphml", new GraphMLResponseWriter());
-    m.put("raw", new RawResponseWriter());
-    m.put(CommonParams.JAVABIN, new JavaBinResponseWriter());
-    m.put("cbor", new CborResponseWriter());
-    m.put("csv", new CSVResponseWriter());
-    m.put("schema.xml", new SchemaXmlResponseWriter());
-    m.put("smile", new SmileResponseWriter());
-    m.put(PROMETHEUS_METRICS_WT, new PrometheusResponseWriter());
-    m.put(OPEN_METRICS_WT, new PrometheusResponseWriter());
-    m.put(ReplicationAPIBase.FILE_STREAM, getFileStreamWriter());
-    DEFAULT_RESPONSE_WRITERS = Collections.unmodifiableMap(m);
-  }
-
-  private static JavaBinResponseWriter getFileStreamWriter() {
-    return new JavaBinResponseWriter() {
-      @Override
-      public void write(
-          OutputStream out, SolrQueryRequest req, SolrQueryResponse response, 
String contentType)
-          throws IOException {
-        RawWriter rawWriter = (RawWriter) 
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
-        if (rawWriter != null) {
-          rawWriter.write(out);
-          if (rawWriter instanceof Closeable) ((Closeable) rawWriter).close();
-        }
-      }
-
-      @Override
-      public String getContentType(SolrQueryRequest request, SolrQueryResponse 
response) {
-        RawWriter rawWriter = (RawWriter) 
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
-        if (rawWriter != null) {
-          return rawWriter.getContentType();
-        } else {
-          return JavaBinResponseParser.JAVABIN_CONTENT_TYPE;
-        }
-      }
-    };
-  }
 
   public void fetchLatestSchema() {
     IndexSchema schema = configSet.getIndexSchema(true);
@@ -3148,11 +3090,48 @@ public class SolrCore implements SolrInfoBean, 
Closeable {
   }
 
   /**
-   * Configure the query response writers. There will always be a default 
writer; additional writers
-   * may also be configured.
+   * Gets a response writer suitable for node/container-level requests.
+   *
+   * @param writerName the writer name, or null for default
+   * @return the response writer, never null
+   * @deprecated Use {@link ResponseWritersRegistry#getWriter(String)} instead.
+   */
+  @Deprecated
+  public static QueryResponseWriter getAdminResponseWriter(String writerName) {
+    return ResponseWritersRegistry.getWriter(writerName);
+  }
+
+  /**
+   * Initializes query response writers. Response writers from {@code 
ImplicitPlugins.json} may also
+   * be configured.
    */
   private void initWriters() {
-    responseWriters.init(DEFAULT_RESPONSE_WRITERS, this);
+    // Build default writers map from implicit plugins
+    Map<String, QueryResponseWriter> defaultWriters = new HashMap<>();
+
+    // Start with built-in writers that are always available
+    defaultWriters.putAll(ResponseWritersRegistry.getAllWriters());
+
+    // Load writers from ImplicitPlugins.json (may override built-ins)
+    List<PluginInfo> implicitWriters = getImplicitResponseWriters();
+    for (PluginInfo info : implicitWriters) {
+      try {
+        QueryResponseWriter writer =
+            createInstance(
+                info.className,
+                QueryResponseWriter.class,
+                "queryResponseWriter",
+                null,
+                getResourceLoader());
+        defaultWriters.put(info.name, writer);
+      } catch (Exception e) {
+        log.warn("Failed to load implicit response writer: {}", info.name, e);
+      }
+    }
+
+    // Initialize with the built defaults
+    responseWriters.init(defaultWriters, this);
+
     // configure the default response writer; this one should never be null
     if (responseWriters.getDefault() == null) 
responseWriters.setDefault("standard");
   }
@@ -3614,32 +3593,49 @@ public class SolrCore implements SolrInfoBean, 
Closeable {
     }
   }
 
-  private static final class ImplicitHolder {
-    private ImplicitHolder() {}
+  private static final class ImplicitPluginsHolder {
+    private ImplicitPluginsHolder() {}
 
-    private static final List<PluginInfo> INSTANCE;
+    private static final Map<String, List<PluginInfo>> ALL_IMPLICIT_PLUGINS;
 
     static {
       @SuppressWarnings("unchecked")
       Map<String, ?> implicitPluginsInfo =
           (Map<String, ?>)
               Utils.fromJSONResource(SolrCore.class.getClassLoader(), 
"ImplicitPlugins.json");
-      @SuppressWarnings("unchecked")
-      Map<String, Map<String, Object>> requestHandlers =
-          (Map<String, Map<String, Object>>) 
implicitPluginsInfo.get(SolrRequestHandler.TYPE);
 
-      List<PluginInfo> implicits = new ArrayList<>(requestHandlers.size());
-      for (Map.Entry<String, Map<String, Object>> entry : 
requestHandlers.entrySet()) {
-        Map<String, Object> info = entry.getValue();
-        info.put(CommonParams.NAME, entry.getKey());
-        implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info));
+      Map<String, List<PluginInfo>> plugins = new HashMap<>();
+
+      // Load all plugin types from the JSON
+      for (Map.Entry<String, ?> entry : implicitPluginsInfo.entrySet()) {
+        String pluginType = entry.getKey();
+        @SuppressWarnings("unchecked")
+        Map<String, Map<String, Object>> pluginConfigs =
+            (Map<String, Map<String, Object>>) entry.getValue();
+
+        List<PluginInfo> pluginInfos = new ArrayList<>(pluginConfigs.size());
+        for (Map.Entry<String, Map<String, Object>> plugin : 
pluginConfigs.entrySet()) {
+          Map<String, Object> info = plugin.getValue();
+          info.put(CommonParams.NAME, plugin.getKey());
+          pluginInfos.add(new PluginInfo(pluginType, info));
+        }
+        plugins.put(pluginType, Collections.unmodifiableList(pluginInfos));
       }
-      INSTANCE = Collections.unmodifiableList(implicits);
+
+      ALL_IMPLICIT_PLUGINS = Collections.unmodifiableMap(plugins);
+    }
+
+    public static List<PluginInfo> getImplicitPlugins(String type) {
+      return ALL_IMPLICIT_PLUGINS.getOrDefault(type, Collections.emptyList());
     }
   }
 
   public List<PluginInfo> getImplicitHandlers() {
-    return ImplicitHolder.INSTANCE;
+    return ImplicitPluginsHolder.getImplicitPlugins(SolrRequestHandler.TYPE);
+  }
+
+  public List<PluginInfo> getImplicitResponseWriters() {
+    return ImplicitPluginsHolder.getImplicitPlugins("queryResponseWriter");
   }
 
   public CancellableQueryTracker getCancellableQueryTracker() {
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
index 16e78ab4268..18447eafac9 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
@@ -77,9 +77,8 @@ public class SystemInfoHandler extends RequestHandlerBase {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   /**
-   * Undocumented expert level system property to prevent doing a reverse 
lookup of our hostname.
-   * This property will be logged as a suggested workaround if any problems 
are noticed when doing
-   * reverse lookup.
+   * Expert level system property to prevent doing a reverse lookup of our 
hostname. This property
+   * will be logged as a suggested workaround if any problems are noticed when 
doing reverse lookup.
    *
    * <p>TODO: should we refactor this (and the associated logic) into a helper 
method for any other
    * places where DNS is used?
@@ -97,7 +96,7 @@ public class SystemInfoHandler extends RequestHandlerBase {
   private static final ConcurrentMap<Class<?>, BeanInfo> beanInfos = new 
ConcurrentHashMap<>();
 
   // on some platforms, resolving canonical hostname can cause the thread
-  // to block for several seconds if nameservices aren't available
+  // to block for several seconds if name services aren't available
   // so resolve this once per handler instance
   // (ie: not static, so core reload will refresh)
   private String hostname = null;
diff --git a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java 
b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
index 10115192fba..0ce4d82e551 100644
--- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
+++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
@@ -31,6 +31,7 @@ import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
 import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.search.SolrIndexSearcher;
 import org.apache.solr.servlet.HttpSolrCall;
@@ -117,7 +118,7 @@ public interface SolrQueryRequest extends AutoCloseable {
   /** The index searcher associated with this request */
   SolrIndexSearcher getSearcher();
 
-  /** The solr core (coordinator, etc) associated with this request */
+  /** The solr core (coordinator, etc.) associated with this request */
   SolrCore getCore();
 
   /** The schema snapshot from core.getLatestSchema() at request creation. */
@@ -145,7 +146,7 @@ public interface SolrQueryRequest extends AutoCloseable {
 
   /**
    * Only for V2 API. Returns a map of path segments and their values. For 
example, if the path is
-   * configured as /path/{segment1}/{segment2} and a reguest is made as 
/path/x/y the returned map
+   * configured as /path/{segment1}/{segment2} and a request is made as 
/path/x/y the returned map
    * would contain {segment1:x ,segment2:y}
    */
   default Map<String, String> getPathTemplateValues() {
@@ -195,16 +196,21 @@ public interface SolrQueryRequest extends AutoCloseable {
     return getCore().getCoreDescriptor().getCloudDescriptor();
   }
 
-  /** The writer to use for this request, considering {@link CommonParams#WT}. 
Never null. */
+  /**
+   * The writer to use for this request, considering {@link CommonParams#WT}. 
Never null.
+   *
+   * <p>If a core is available, uses the core's response writer registry. If 
no core is available
+   * (e.g., for node/container requests), uses a minimal set of 
node/container-appropriate writers.
+   */
   default QueryResponseWriter getResponseWriter() {
     // it's weird this method is here instead of SolrQueryResponse, but it's 
practical/convenient
     SolrCore core = getCore();
     String wt = getParams().get(CommonParams.WT);
+    // Use core writers if available, otherwise fall back to built-in writers
     if (core != null) {
       return core.getQueryResponseWriter(wt);
     } else {
-      return SolrCore.DEFAULT_RESPONSE_WRITERS.getOrDefault(
-          wt, SolrCore.DEFAULT_RESPONSE_WRITERS.get("standard"));
+      return ResponseWritersRegistry.getWriter(wt);
     }
   }
 
diff --git 
a/solr/core/src/java/org/apache/solr/response/FileStreamResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/FileStreamResponseWriter.java
new file mode 100644
index 00000000000..91f6ee12f33
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/FileStreamResponseWriter.java
@@ -0,0 +1,67 @@
+/*
+ * 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.response;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.solr.client.solrj.response.JavaBinResponseParser;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.admin.api.ReplicationAPIBase;
+import org.apache.solr.request.SolrQueryRequest;
+
+/**
+ * Response writer for file streaming operations, used for replication, 
exports, and other core Solr
+ * operations.
+ *
+ * <p>This writer handles streaming of large files (such as index files) by 
looking for a {@link
+ * org.apache.solr.core.SolrCore.RawWriter} object in the response under the 
{@link
+ * ReplicationAPIBase#FILE_STREAM} key. When found, it delegates directly to 
the raw writer to
+ * stream the file content efficiently.
+ *
+ * <p>This writer is specifically designed for replication file transfers and 
provides no fallback
+ * behavior - it only works when a proper RawWriter is present in the response.
+ */
+public class FileStreamResponseWriter implements QueryResponseWriter {
+
+  @Override
+  public void write(
+      OutputStream out, SolrQueryRequest request, SolrQueryResponse response, 
String contentType)
+      throws IOException {
+    SolrCore.RawWriter rawWriter =
+        (SolrCore.RawWriter) 
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
+    if (rawWriter != null) {
+      rawWriter.write(out);
+      if (rawWriter instanceof Closeable closeable) {
+        closeable.close();
+      }
+    }
+  }
+
+  @Override
+  public String getContentType(SolrQueryRequest request, SolrQueryResponse 
response) {
+    SolrCore.RawWriter rawWriter =
+        (SolrCore.RawWriter) 
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
+    if (rawWriter != null) {
+      String contentType = rawWriter.getContentType();
+      if (contentType != null) {
+        return contentType;
+      }
+    }
+    return JavaBinResponseParser.JAVABIN_CONTENT_TYPE;
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java 
b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
new file mode 100644
index 00000000000..cfd04e28714
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
@@ -0,0 +1,93 @@
+/*
+ * 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.response;
+
+import static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT;
+import static 
org.apache.solr.handler.admin.MetricsHandler.PROMETHEUS_METRICS_WT;
+
+import java.util.Map;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.handler.admin.api.ReplicationAPIBase;
+
+/**
+ * Essential response writers always available regardless of core 
configuration.
+ *
+ * <p>Used by node/container-level requests that have no associated {@link
+ * org.apache.solr.core.SolrCore}.
+ *
+ * <p>For the full set of response writers see {@link 
org.apache.solr.core.SolrCore}'s response
+ * writer registry.
+ */
+public class ResponseWritersRegistry {
+
+  private ResponseWritersRegistry() {
+    // Prevent instantiation
+  }
+
+  private static final Map<String, QueryResponseWriter> BUILTIN_WRITERS;
+
+  static {
+    // Initialize built-in writers that are always available
+    JacksonJsonWriter jsonWriter = new JacksonJsonWriter();
+    PrometheusResponseWriter prometheusWriter = new PrometheusResponseWriter();
+
+    BUILTIN_WRITERS =
+        Map.of(
+            CommonParams.JAVABIN,
+            new JavaBinResponseWriter(),
+            CommonParams.JSON,
+            jsonWriter,
+            "standard",
+            jsonWriter, // Alias for JSON
+            "xml",
+            new XMLResponseWriter(),
+            PROMETHEUS_METRICS_WT,
+            prometheusWriter,
+            OPEN_METRICS_WT,
+            prometheusWriter,
+            ReplicationAPIBase.FILE_STREAM,
+            new FileStreamResponseWriter());
+  }
+
+  /**
+   * Gets a built-in response writer.
+   *
+   * <p>Built-in writers are always available and provide essential formats 
needed by admin APIs and
+   * core functionality. They do not depend on core configuration or 
ImplicitPlugins.json settings.
+   *
+   * <p>If the requested writer is not available, returns the "standard" 
(JSON) writer as a
+   * fallback. This ensures requests always get a valid response format.
+   *
+   * @param writerName the writer name (e.g., "json", "xml", "javabin"), or 
null for default
+   * @return the response writer, never null (returns "standard"/JSON if not 
found)
+   */
+  public static QueryResponseWriter getWriter(String writerName) {
+    if (writerName == null || writerName.isEmpty()) {
+      return BUILTIN_WRITERS.get("standard");
+    }
+    return BUILTIN_WRITERS.getOrDefault(writerName, 
BUILTIN_WRITERS.get("standard"));
+  }
+
+  /**
+   * Gets all built-in response writers.
+   *
+   * @return immutable map of all built-in writers
+   */
+  public static Map<String, QueryResponseWriter> getAllWriters() {
+    return BUILTIN_WRITERS;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java 
b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
index f799859a263..5f2b67622d6 100644
--- a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
+++ b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
@@ -349,7 +349,7 @@ public class SolrQueryResponse {
    *
    * @param name the name of the header
    * @param value the header value If it contains octet string, it should be 
encoded according to
-   *     RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt)
+   *     RFC 2047 (<a href="http://www.ietf.org/rfc/rfc2047.txt";>...</a>)
    * @see #addHttpHeader
    * @see HttpServletResponse#setHeader
    */
@@ -364,7 +364,7 @@ public class SolrQueryResponse {
    *
    * @param name the name of the header
    * @param value the additional header value If it contains octet string, it 
should be encoded
-   *     according to RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt)
+   *     according to RFC 2047 (<a 
href="http://www.ietf.org/rfc/rfc2047.txt";>...</a>)
    * @see #setHttpHeader
    * @see HttpServletResponse#addHeader
    */
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 db998ff9b8c..1229aed8d0a 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -86,6 +86,7 @@ import org.apache.solr.request.SolrQueryRequestBase;
 import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.request.SolrRequestInfo;
 import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuditEvent;
 import org.apache.solr.security.AuditEvent.EventType;
@@ -735,8 +736,9 @@ public class HttpSolrCall {
             solrResp.getToLogAsString("[admin]"));
       }
     }
+    // node/container requests have no core, use built-in writers
     QueryResponseWriter respWriter =
-        
SolrCore.DEFAULT_RESPONSE_WRITERS.get(solrReq.getParams().get(CommonParams.WT));
+        
ResponseWritersRegistry.getWriter(solrReq.getParams().get(CommonParams.WT));
     if (respWriter == null) respWriter = getResponseWriter();
     writeResponse(solrResp, respWriter, Method.getMethod(req.getMethod()));
     if (shouldAudit()) {
diff --git a/solr/core/src/resources/ImplicitPlugins.json 
b/solr/core/src/resources/ImplicitPlugins.json
index 4154e70ded9..eba9bd05ba0 100644
--- a/solr/core/src/resources/ImplicitPlugins.json
+++ b/solr/core/src/resources/ImplicitPlugins.json
@@ -157,5 +157,25 @@
         "activetaskslist"
       ]
     }
+  },
+  "queryResponseWriter": {
+    "geojson": {
+      "class": "solr.GeoJSONResponseWriter"
+    },
+    "graphml": {
+      "class": "solr.GraphMLResponseWriter"
+    },
+    "cbor": {
+      "class": "solr.CborResponseWriter"
+    },
+    "csv": {
+      "class": "solr.CSVResponseWriter"
+    },
+    "schema.xml": {
+      "class": "solr.SchemaXmlResponseWriter"
+    },
+    "smile": {
+      "class": "solr.SmileResponseWriter"
+    }
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/core/TestImplicitPlugins.java 
b/solr/core/src/test/org/apache/solr/core/TestImplicitPlugins.java
new file mode 100644
index 00000000000..a04604b22d7
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/core/TestImplicitPlugins.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.core;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests for implicit plugins loaded from ImplicitPlugins.json.
+ *
+ * <p>This test class verifies:
+ *
+ * <ul>
+ *   <li>Request handlers are loaded from ImplicitPlugins.json
+ *   <li>Response writers are loaded from ImplicitPlugins.json for core 
requests
+ *   <li>Built in response writers use a minimal set defined in {@link 
ResponseWritersRegistry}.
+ * </ul>
+ */
+public class TestImplicitPlugins extends SolrTestCaseJ4 {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig.xml", "schema.xml");
+  }
+
+  // ========== Core vs Built-in Writer Separation Tests ==========
+
+  @Test
+  public void testCoreAndBuiltInWriterIntegration() {
+    final SolrCore core = h.getCore();
+
+    // Test that core has extended writers from ImplicitPlugins.json
+    assertNotNull("Core should have csv writer", 
core.getQueryResponseWriter("csv"));
+    assertNotNull("Core should have geojson writer", 
core.getQueryResponseWriter("geojson"));
+    assertNotNull("Core should have graphml writer", 
core.getQueryResponseWriter("graphml"));
+    assertNotNull("Core should have smile writer", 
core.getQueryResponseWriter("smile"));
+
+    // Test that built-in registry has minimal set and falls back for extended 
formats
+    QueryResponseWriter standardWriter = 
ResponseWritersRegistry.getWriter("standard");
+    assertSame(
+        "Built-in csv request should fall back to standard",
+        standardWriter,
+        ResponseWritersRegistry.getWriter("csv"));
+    assertSame(
+        "Built-in geojson request should fall back to standard",
+        standardWriter,
+        ResponseWritersRegistry.getWriter("geojson"));
+
+    // Test that both systems have common essential formats (though may be 
different instances)
+    QueryResponseWriter coreJsonWriter = 
core.getQueryResponseWriter(CommonParams.JSON);
+    QueryResponseWriter builtInJsonWriter = 
ResponseWritersRegistry.getWriter(CommonParams.JSON);
+    assertNotNull("Core json writer should not be null", coreJsonWriter);
+    assertNotNull("Built-in json writer should not be null", 
builtInJsonWriter);
+
+    QueryResponseWriter coreXmlWriter = core.getQueryResponseWriter("xml");
+    QueryResponseWriter builtInXmlWriter = 
ResponseWritersRegistry.getWriter("xml");
+    assertNotNull("Core xml writer should not be null", coreXmlWriter);
+    assertNotNull("Built-in xml writer should not be null", builtInXmlWriter);
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestFileStreamResponseWriter.java 
b/solr/core/src/test/org/apache/solr/response/TestFileStreamResponseWriter.java
new file mode 100644
index 00000000000..5852aefbe14
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/response/TestFileStreamResponseWriter.java
@@ -0,0 +1,149 @@
+/*
+ * 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.response;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.response.JavaBinResponseParser;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.admin.api.ReplicationAPIBase;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequest;
+import org.junit.Test;
+
+public class TestFileStreamResponseWriter extends SolrTestCase {
+
+  @Test
+  public void testWriteWithRawWriter() throws IOException {
+    FileStreamResponseWriter writer = new FileStreamResponseWriter();
+    SolrQueryRequest request = new LocalSolrQueryRequest(null, new 
ModifiableSolrParams());
+    SolrQueryResponse response = new SolrQueryResponse();
+
+    // Create a mock RawWriter
+    String testContent = "test file content";
+    TestRawWriter rawWriter = new TestRawWriter(testContent, 
"application/octet-stream");
+
+    // Add the RawWriter to the response
+    response.add(ReplicationAPIBase.FILE_STREAM, rawWriter);
+
+    // Write to output stream
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    writer.write(out, request, response, null);
+
+    // Verify the content was written
+    String written = out.toString(StandardCharsets.UTF_8);
+    assertEquals("Content should be written directly", testContent, written);
+  }
+
+  @Test
+  public void testWriteWithoutRawWriter() throws IOException {
+    FileStreamResponseWriter writer = new FileStreamResponseWriter();
+    SolrQueryRequest request = new LocalSolrQueryRequest(null, new 
ModifiableSolrParams());
+    SolrQueryResponse response = new SolrQueryResponse();
+
+    // Don't add any RawWriter to the response
+
+    // Write to output stream
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    writer.write(out, request, response, null);
+
+    // Verify nothing was written (since no RawWriter present)
+    assertEquals("Nothing should be written when no RawWriter present", 0, 
out.size());
+  }
+
+  @Test
+  public void testGetContentTypeWithRawWriter() {
+    FileStreamResponseWriter writer = new FileStreamResponseWriter();
+    SolrQueryRequest request = new LocalSolrQueryRequest(null, new 
ModifiableSolrParams());
+    SolrQueryResponse response = new SolrQueryResponse();
+
+    // Create a mock RawWriter with custom content type
+    String customContentType = "application/custom-type";
+    TestRawWriter rawWriter = new TestRawWriter("content", customContentType);
+
+    // Add the RawWriter to the response
+    response.add(ReplicationAPIBase.FILE_STREAM, rawWriter);
+
+    // Get content type
+    String contentType = writer.getContentType(request, response);
+    assertEquals("Should return RawWriter's content type", customContentType, 
contentType);
+  }
+
+  @Test
+  public void testGetContentTypeWithoutRawWriter() {
+    FileStreamResponseWriter writer = new FileStreamResponseWriter();
+    SolrQueryRequest request = new LocalSolrQueryRequest(null, new 
ModifiableSolrParams());
+    SolrQueryResponse response = new SolrQueryResponse();
+
+    // Don't add any RawWriter to the response
+
+    // Get content type
+    String contentType = writer.getContentType(request, response);
+    assertEquals(
+        "Should return default javabin content type",
+        JavaBinResponseParser.JAVABIN_CONTENT_TYPE,
+        contentType);
+  }
+
+  @Test
+  public void testGetContentTypeWithRawWriterReturningNull() {
+    FileStreamResponseWriter writer = new FileStreamResponseWriter();
+    SolrQueryRequest request = new LocalSolrQueryRequest(null, new 
ModifiableSolrParams());
+    SolrQueryResponse response = new SolrQueryResponse();
+
+    // Create a mock RawWriter that returns null for content type
+    TestRawWriter rawWriter = new TestRawWriter("content", null);
+
+    // Add the RawWriter to the response
+    response.add(ReplicationAPIBase.FILE_STREAM, rawWriter);
+
+    // Get content type
+    String contentType = writer.getContentType(request, response);
+    assertEquals(
+        "Should return default javabin content type when RawWriter returns 
null",
+        JavaBinResponseParser.JAVABIN_CONTENT_TYPE,
+        contentType);
+  }
+
+  // Test helper classes
+  // Avoids standing up a full Solr core for this test by mocking.
+  private static class TestRawWriter implements SolrCore.RawWriter {
+    private final String content;
+    private final String contentType;
+
+    public TestRawWriter(String content, String contentType) {
+      this.content = content;
+      this.contentType = contentType;
+    }
+
+    @Override
+    public String getContentType() {
+      return contentType;
+    }
+
+    @Override
+    public void write(OutputStream os) throws IOException {
+      if (content != null) {
+        os.write(content.getBytes(StandardCharsets.UTF_8));
+      }
+    }
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java 
b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
new file mode 100644
index 00000000000..695ad0e1278
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
@@ -0,0 +1,64 @@
+/*
+ * 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.response;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+
+/**
+ * This test validates the registry's behavior for built-in response writers, 
including
+ * availability, fallback behavior, and proper format handling. Notice there 
is no core configured!
+ */
+public class TestResponseWritersRegistry extends SolrTestCaseJ4 {
+
+  @Test
+  public void testBuiltInWriterFallbackBehavior() {
+    QueryResponseWriter standardWriter = 
ResponseWritersRegistry.getWriter("standard");
+
+    // Test null fallback
+    QueryResponseWriter nullWriter = ResponseWritersRegistry.getWriter(null);
+    assertThat("null writer should not be null", nullWriter, 
is(not(nullValue())));
+    assertThat("null writer should be same as standard", nullWriter, 
is(standardWriter));
+
+    // Test empty string fallback
+    QueryResponseWriter emptyWriter = ResponseWritersRegistry.getWriter("");
+    assertThat("empty writer should not be null", emptyWriter, 
is(not(nullValue())));
+    assertThat("empty writer should be same as standard", emptyWriter, 
is(standardWriter));
+
+    // Test unknown format fallback
+    QueryResponseWriter unknownWriter = 
ResponseWritersRegistry.getWriter("nonexistent");
+    assertThat("unknown writer should not be null", unknownWriter, 
is(not(nullValue())));
+    assertThat("unknown writer should be same as standard", unknownWriter, 
is(standardWriter));
+  }
+
+  @Test
+  public void testBuiltInWriterLimitedSet() {
+    QueryResponseWriter standardWriter = 
ResponseWritersRegistry.getWriter("standard");
+
+    // Built-in writers should NOT include extended format writers (csv, 
geojson, etc.)
+    // These should all fall back to standard
+    // I think this standard thing is weird...   I think it should throw an 
exception!
+    assertThat(
+        "geojson should fall back to standard",
+        ResponseWritersRegistry.getWriter("geojson"),
+        is(standardWriter));
+  }
+}


Reply via email to