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

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

commit a7f40683e60f8d3609a6c86b84442ccf9489785f
Author: Eric Pugh <[email protected]>
AuthorDate: Tue Feb 10 11:08:55 2026 -0500

    SOLR-18085: Remove use of "wt=standard" internally to Solr. (#4080)
    
    Updates Solr’s response-writer (wt) handling to remove reliance on the 
legacy "standard" writer and to fail fast when an unknown response writer is 
requested.  Additionally we migrate from throwing a 500 error to a more 
appropriate 400 error when a non existing response-write is specified.
    
    (cherry picked from commit 4f40e977a337d440c598b6f37156045e5bd8a926)
---
 changelog/unreleased/SOLR-18085.yml                |   8 +
 .../src/java/org/apache/solr/core/PluginBag.java   |   2 +-
 .../java/org/apache/solr/core/RequestHandlers.java |   2 +-
 .../src/java/org/apache/solr/core/SolrCore.java    |   7 +-
 .../org/apache/solr/request/SolrQueryRequest.java  |   9 +-
 .../apache/solr/response/RawResponseWriter.java    |   9 +-
 .../solr/response/ResponseWritersRegistry.java     |  88 ++++++++---
 .../java/org/apache/solr/servlet/HttpSolrCall.java |   2 +-
 .../apache/solr/servlet/SolrRequestParsers.java    |  56 ++-----
 .../src/test/org/apache/solr/OutputWriterTest.java |  21 +--
 .../apache/solr/handler/V2ApiIntegrationTest.java  |  35 ++---
 .../response/TestPrometheusResponseWriter.java     |   1 +
 .../solr/response/TestRawResponseWriter.java       |  13 +-
 .../solr/response/TestResponseWritersRegistry.java |  23 +--
 solr/packaging/test/test_rolling_upgrade.bats      | 172 +++++++++++++++++++++
 .../src/java/org/apache/solr/util/TestHarness.java |   1 +
 16 files changed, 305 insertions(+), 144 deletions(-)

diff --git a/changelog/unreleased/SOLR-18085.yml 
b/changelog/unreleased/SOLR-18085.yml
new file mode 100644
index 00000000000..d6ca801ddc8
--- /dev/null
+++ b/changelog/unreleased/SOLR-18085.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Removed the wt=standard concept that was used internally by Solr.
+type: removed # added, changed, fixed, deprecated, removed, dependency_update, 
security, other
+authors:
+  - name: Eric Pugh
+links:
+  - name: SOLR-18085
+    url: https://issues.apache.org/jira/browse/SOLR-18085
diff --git a/solr/core/src/java/org/apache/solr/core/PluginBag.java 
b/solr/core/src/java/org/apache/solr/core/PluginBag.java
index 7624a4c13dd..1af3445e7aa 100644
--- a/solr/core/src/java/org/apache/solr/core/PluginBag.java
+++ b/solr/core/src/java/org/apache/solr/core/PluginBag.java
@@ -202,7 +202,7 @@ public class PluginBag<T> implements AutoCloseable {
    * Fetches a plugin by name , or the default
    *
    * @param name name using which it is registered
-   * @param useDefault Return the default , if a plugin by that name does not 
exist
+   * @param useDefault Return the default, if a plugin by that name does not 
exist
    */
   public T get(String name, boolean useDefault) {
     T result = get(name);
diff --git a/solr/core/src/java/org/apache/solr/core/RequestHandlers.java 
b/solr/core/src/java/org/apache/solr/core/RequestHandlers.java
index dca7c832e3f..39f043d95f9 100644
--- a/solr/core/src/java/org/apache/solr/core/RequestHandlers.java
+++ b/solr/core/src/java/org/apache/solr/core/RequestHandlers.java
@@ -117,7 +117,7 @@ public final class RequestHandlers {
       modifiedInfos.add(applyInitParams(config, info));
     }
     handlers.init(Collections.emptyMap(), core, modifiedInfos);
-    handlers.alias(handlers.getDefault(), "");
+
     if (log.isDebugEnabled()) {
       log.debug("Registered paths: {}", StrUtils.join(new 
ArrayList<>(handlers.keySet()), ','));
     }
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 fc50c62cb1f..264643ff2bf 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -3128,14 +3128,11 @@ public class SolrCore implements SolrInfoBean, 
Closeable {
 
     // 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");
   }
 
-  /** Finds a writer by name, or returns the default writer if not found. */
+  /** Finds a writer by name, or null if not found. */
   public final QueryResponseWriter getQueryResponseWriter(String writerName) {
-    return responseWriters.get(writerName, true);
+    return responseWriters.get(writerName, false);
   }
 
   /**
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 0ce4d82e551..c4aecf0c47e 100644
--- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
+++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
@@ -204,14 +204,7 @@ public interface SolrQueryRequest extends AutoCloseable {
    */
   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 ResponseWritersRegistry.getWriter(wt);
-    }
+    return ResponseWritersRegistry.getWriter(getParams().get(CommonParams.WT), 
getCore());
   }
 
   /**
diff --git a/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java
index 922b4932952..fe5269497ff 100644
--- a/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java
@@ -47,7 +47,7 @@ public class RawResponseWriter implements QueryResponseWriter 
{
    */
   public static final String CONTENT = "content";
 
-  private String _baseWriter = null;
+  private String baseWriter = null;
 
   /**
    * A fallback writer used for requests that don't return raw content and 
that aren't associated
@@ -62,14 +62,17 @@ public class RawResponseWriter implements 
QueryResponseWriter {
     if (n != null) {
       Object base = n.get("base");
       if (base != null) {
-        _baseWriter = base.toString();
+        baseWriter = base.toString();
       }
     }
   }
 
   protected QueryResponseWriter getBaseWriter(SolrQueryRequest request) {
     if (request.getCore() != null) {
-      return request.getCore().getQueryResponseWriter(_baseWriter);
+      // When baseWriter is null, use the core's default writer 
(useDefault=true)
+      // Otherwise, look up the specific writer by name (useDefault=false for 
explicit lookups)
+      boolean useDefault = (baseWriter == null);
+      return request.getCore().getResponseWriters().get(baseWriter, 
useDefault);
     }
 
     // Requests to a specific core already have writers, but we still need a 
'default writer' for
diff --git 
a/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java 
b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
index 67ec648cbf6..230b8caf99c 100644
--- a/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
+++ b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
@@ -20,7 +20,9 @@ import static 
org.apache.solr.util.stats.MetricUtils.OPEN_METRICS_WT;
 import static org.apache.solr.util.stats.MetricUtils.PROMETHEUS_METRICS_WT;
 
 import java.util.Map;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.core.SolrCore;
 import org.apache.solr.handler.admin.api.ReplicationAPIBase;
 
 /**
@@ -46,23 +48,14 @@ public class ResponseWritersRegistry {
     PrometheusResponseWriter prometheusWriter = new PrometheusResponseWriter();
 
     BUILTIN_WRITERS =
-        Map.of(
-            CommonParams.JAVABIN,
-            new JavaBinResponseWriter(),
-            CommonParams.JSON,
-            jsonWriter,
-            "standard",
-            jsonWriter, // Alias for JSON
-            "xml",
-            new XMLResponseWriter(),
-            "raw",
-            new RawResponseWriter(),
-            PROMETHEUS_METRICS_WT,
-            prometheusWriter,
-            OPEN_METRICS_WT,
-            prometheusWriter,
-            ReplicationAPIBase.FILE_STREAM,
-            new FileStreamResponseWriter());
+        Map.ofEntries(
+            Map.entry(CommonParams.JAVABIN, new JavaBinResponseWriter()),
+            Map.entry(CommonParams.JSON, jsonWriter),
+            Map.entry("xml", new XMLResponseWriter()),
+            Map.entry("raw", new RawResponseWriter()),
+            Map.entry(PROMETHEUS_METRICS_WT, prometheusWriter),
+            Map.entry(OPEN_METRICS_WT, prometheusWriter),
+            Map.entry(ReplicationAPIBase.FILE_STREAM, new 
FileStreamResponseWriter()));
   }
 
   /**
@@ -71,17 +64,68 @@ public class ResponseWritersRegistry {
    * <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.
+   * <p>If the requested writer is not available, returns the 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)
+   * @return the response writer, never null (returns JSON if not found)
    */
   public static QueryResponseWriter getWriter(String writerName) {
     if (writerName == null || writerName.isEmpty()) {
-      return BUILTIN_WRITERS.get("standard");
+      writerName = CommonParams.JSON;
     }
-    return BUILTIN_WRITERS.getOrDefault(writerName, 
BUILTIN_WRITERS.get("standard"));
+    return BUILTIN_WRITERS.get(writerName);
+  }
+
+  /**
+   * Gets a response writer, trying the core's registry first, then falling 
back to built-in
+   * writers. This is the unified entry point for all writer resolution.
+   *
+   * <p>Resolution order:
+   *
+   * <ol>
+   *   <li>If core is provided, check core's writer registry
+   *   <li>If not found in core (or no core), check built-in writers
+   *   <li>If writer name is explicitly specified but not found anywhere, 
throw exception
+   *   <li>If writer name is null/empty, return default (JSON)
+   * </ol>
+   *
+   * @param writerName the writer name (e.g., "json", "xml", "javabin"), or 
null for default
+   * @param core the SolrCore to check first, or null for node-level requests
+   * @return the response writer, never null
+   * @throws SolrException if an explicitly requested writer type is not found
+   */
+  public static QueryResponseWriter getWriter(String writerName, SolrCore 
core) {
+    QueryResponseWriter writer = null;
+
+    // Try core registry first if available
+    if (core != null) {
+      writer = core.getQueryResponseWriter(writerName);
+    }
+
+    // If not found and writer is explicitly requested, validate it exists in 
built-in
+    if (writer == null) {
+      if (!hasWriter(writerName)) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Unknown response writer 
type: " + writerName);
+      } else {
+        writer = getWriter(writerName);
+      }
+    }
+    return writer;
+  }
+
+  /**
+   * Checks if a writer with the given name exists in the built-in writers.
+   *
+   * @param writerName the writer name to check
+   * @return true if the writer exists, false otherwise
+   */
+  public static boolean hasWriter(String writerName) {
+    if (writerName == null || writerName.isEmpty()) {
+      return true; // null/empty is valid, will use default
+    }
+    return BUILTIN_WRITERS.containsKey(writerName);
   }
 
   /**
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 a77541be552..c289ca6a3a9 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -727,7 +727,7 @@ public class HttpSolrCall {
 
   protected void logAndFlushAdminRequest(SolrQueryResponse solrResp) throws 
IOException {
     if (solrResp.getToLog().size() > 0) {
-      // has to come second and in it's own if to keep ./gradlew check happy.
+      // has to come second and in its own "if" to keep ./gradlew check happy.
       if (log.isInfoEnabled()) {
         log.info(
             handler != null
diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java 
b/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java
index 3cf12aa7982..e835d844db8 100644
--- a/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java
+++ b/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java
@@ -67,13 +67,6 @@ public class SolrRequestParsers {
 
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  // Should these constants be in a more public place?
-  public static final String MULTIPART = "multipart";
-  public static final String FORMDATA = "formdata";
-  public static final String RAW = "raw";
-  public static final String SIMPLE = "simple";
-  public static final String STANDARD = "standard";
-
   private static final Charset CHARSET_US_ASCII = StandardCharsets.US_ASCII;
 
   public static final String INPUT_ENCODING_KEY = "ie";
@@ -81,8 +74,7 @@ public class SolrRequestParsers {
 
   public static final String REQUEST_TIMER_SERVLET_ATTRIBUTE = 
"org.apache.solr.RequestTimer";
 
-  private final HashMap<String, SolrRequestParser> parsers = new HashMap<>();
-  private StandardRequestParser standard;
+  private StandardRequestParser parser;
 
   /**
    * Default instance for e.g. admin requests. Limits to 2 MB uploads and does 
not allow remote
@@ -91,7 +83,7 @@ public class SolrRequestParsers {
   public static final SolrRequestParsers DEFAULT = new SolrRequestParsers();
 
   /**
-   * Pass in an xml configuration. A null configuration will enable everything 
with maximum values.
+   * Pass in a xml configuration. A null configuration will enable everything 
with maximum values.
    */
   public SolrRequestParsers(SolrConfig globalConfig) {
     final int multipartUploadLimitKB, formUploadLimitKB;
@@ -116,21 +108,12 @@ public class SolrRequestParsers {
     MultipartRequestParser multi = new 
MultipartRequestParser(multipartUploadLimitKB);
     RawRequestParser raw = new RawRequestParser();
     FormDataRequestParser formdata = new 
FormDataRequestParser(formUploadLimitKB);
-    standard = new StandardRequestParser(multi, raw, formdata);
-
-    // I don't see a need to have this publicly configured just yet
-    // adding it is trivial
-    parsers.put(MULTIPART, multi);
-    parsers.put(FORMDATA, formdata);
-    parsers.put(RAW, raw);
-    parsers.put(SIMPLE, new SimpleRequestParser());
-    parsers.put(STANDARD, standard);
-    parsers.put("", standard);
+    parser = new StandardRequestParser(multi, raw, formdata);
   }
 
   private static RTimerTree getRequestTimer(HttpServletRequest req) {
     final Object reqTimer = req.getAttribute(REQUEST_TIMER_SERVLET_ATTRIBUTE);
-    if (reqTimer != null && reqTimer instanceof RTimerTree) {
+    if (reqTimer instanceof RTimerTree) {
       return ((RTimerTree) reqTimer);
     }
 
@@ -139,7 +122,6 @@ public class SolrRequestParsers {
 
   public SolrQueryRequest parse(SolrCore core, String path, HttpServletRequest 
req)
       throws Exception {
-    SolrRequestParser parser = standard;
 
     // TODO -- in the future, we could pick a different parser based on the 
request
 
@@ -164,13 +146,12 @@ public class SolrRequestParsers {
 
   /** For embedded Solr use; not related to HTTP. */
   public SolrQueryRequest buildRequestFrom(
-      SolrCore core, SolrParams params, Collection<ContentStream> streams) 
throws Exception {
+      SolrCore core, SolrParams params, Collection<ContentStream> streams) {
     return buildRequestFrom(core, params, streams, new RTimerTree(), null, 
null);
   }
 
   public SolrQueryRequest buildRequestFrom(
-      SolrCore core, SolrParams params, Collection<ContentStream> streams, 
Principal principal)
-      throws Exception {
+      SolrCore core, SolrParams params, Collection<ContentStream> streams, 
Principal principal) {
     return buildRequestFrom(core, params, streams, new RTimerTree(), null, 
principal);
   }
 
@@ -181,7 +162,7 @@ public class SolrRequestParsers {
       RTimerTree requestTimer,
       final HttpServletRequest req,
       final Principal principal) // from req, if req was provided, otherwise 
from elsewhere
-      throws Exception {
+      {
     // ensure streams is non-null and mutable so we can easily add to it
     if (streams == null) {
       streams = new ArrayList<>();
@@ -213,7 +194,7 @@ public class SolrRequestParsers {
 
           @Override
           public Map<String, String> getPathTemplateValues() {
-            if (httpSolrCall != null && httpSolrCall instanceof V2HttpCall) {
+            if (httpSolrCall instanceof V2HttpCall) {
               return ((V2HttpCall) httpSolrCall).getUrlParts();
             }
             return super.getPathTemplateValues();
@@ -337,9 +318,9 @@ public class SolrRequestParsers {
               // we have no charset decoder until now, buffer the keys / 
values for later
               // processing:
               buffer.add(keyBytes);
-              buffer.add(Long.valueOf(keyPos));
+              buffer.add(keyPos);
               buffer.add(valueBytes);
-              buffer.add(Long.valueOf(valuePos));
+              buffer.add(valuePos);
             } else {
               // we already have a charsetDecoder, so we can directly decode 
without buffering:
               final String key = decodeChars(keyBytes, keyPos, charsetDecoder),
@@ -457,7 +438,7 @@ public class SolrRequestParsers {
   // -----------------------------------------------------------------
   // -----------------------------------------------------------------
 
-  // I guess we don't really even need the interface, but i'll keep it here 
just for kicks
+  // I guess we don't really even need the interface, but I'll keep it here 
just for kicks
   interface SolrRequestParser {
     public SolrParams parseParamsAndFillStreams(
         final HttpServletRequest req, ArrayList<ContentStream> streams) throws 
Exception;
@@ -466,15 +447,6 @@ public class SolrRequestParsers {
   // -----------------------------------------------------------------
   // -----------------------------------------------------------------
 
-  /** The simple parser just uses the params directly, does not support POST 
URL-encoded forms */
-  static class SimpleRequestParser implements SolrRequestParser {
-    @Override
-    public SolrParams parseParamsAndFillStreams(
-        final HttpServletRequest req, ArrayList<ContentStream> streams) throws 
Exception {
-      return parseQueryString(req.getQueryString());
-    }
-  }
-
   /** Wrap an HttpServletRequest as a ContentStream */
   static class HttpRequestContentStream extends ContentStreamBase {
     private final InputStream inputStream;
@@ -515,7 +487,7 @@ public class SolrRequestParsers {
           || req.getHeader("Transfer-Encoding") != null
           || !NO_BODY_METHODS.contains(req.getMethod())) {
         // If Content-Length > 0 OR Transfer-Encoding exists OR
-        // it's a method that can have a body (POST/PUT/PATCH etc)
+        // it's a method that can have a body (POST/PUT/PATCH etc.)
         streams.add(new HttpRequestContentStream(req, req.getInputStream()));
       }
 
@@ -543,7 +515,7 @@ public class SolrRequestParsers {
         throw new SolrException(
             ErrorCode.BAD_REQUEST, "Not multipart content! " + 
req.getContentType());
       }
-      // Magic way to tell Jetty dynamically we want multi-part processing.
+      // Magic way to tell Jetty dynamically we want multipart processing.
       // This is taken from:
       // 
https://github.com/eclipse/jetty.project/blob/jetty-10.0.12/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java#L144
       req.setAttribute("org.eclipse.jetty.multipartConfig", 
multipartConfigElement);
@@ -735,7 +707,7 @@ public class SolrRequestParsers {
     public SolrParams parseParamsAndFillStreams(
         final HttpServletRequest req, ArrayList<ContentStream> streams) throws 
Exception {
       String contentType = req.getContentType();
-      String method = req.getMethod(); // No need to uppercase... HTTP verbs 
are case sensitive
+      String method = req.getMethod(); // No need to uppercase... HTTP verbs 
are case-sensitive
       String uri = req.getRequestURI();
       boolean isV2 = getHttpSolrCall(req) instanceof V2HttpCall;
       boolean isPost = "POST".equals(method);
diff --git a/solr/core/src/test/org/apache/solr/OutputWriterTest.java 
b/solr/core/src/test/org/apache/solr/OutputWriterTest.java
index 30df9ed98fa..7f711631ce1 100644
--- a/solr/core/src/test/org/apache/solr/OutputWriterTest.java
+++ b/solr/core/src/test/org/apache/solr/OutputWriterTest.java
@@ -29,32 +29,16 @@ import org.junit.Test;
 /** Tests the ability to configure multiple query output writers, and select 
those at query time. */
 public class OutputWriterTest extends SolrTestCaseJ4 {
 
-  /** The XML string that's output for testing purposes. */
-  public static final String USELESS_OUTPUT = "useless output";
-
   @BeforeClass
   public static void beforeClass() throws Exception {
     initCore("solr/crazy-path-to-config.xml", "solr/crazy-path-to-schema.xml");
   }
 
-  /**
-   * responseHeader has changed in SOLR-59, check old and new variants, In 
SOLR-2413, we removed
-   * support for the deprecated versions
-   */
-  @Test
-  public void testSOLR59responseHeaderVersions() {
-    // default results in "new" responseHeader
-    lrf.args.put("wt", "standard");
-    assertQ(req("foo"), 
"/response/lst[@name='responseHeader']/int[@name='status'][.='0']");
-    lrf.args.remove("wt");
-    assertQ(req("foo"), 
"/response/lst[@name='responseHeader']/int[@name='QTime']");
-  }
-
   @Test
   public void testUselessWriter() throws Exception {
     lrf.args.put("wt", "useless");
     String out = h.query(req("foo"));
-    assertEquals(USELESS_OUTPUT, out);
+    assertEquals(UselessOutputWriter.USELESS_OUTPUT, out);
   }
 
   public void testLazy() {
@@ -71,6 +55,9 @@ public class OutputWriterTest extends SolrTestCaseJ4 {
   /** An output writer that doesn't do anything useful. */
   public static class UselessOutputWriter implements TextQueryResponseWriter {
 
+    /** The XML string that's output for testing purposes. */
+    public static final String USELESS_OUTPUT = "useless output";
+
     public UselessOutputWriter() {}
 
     @Override
diff --git 
a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java 
b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
index 3b7a85eb096..4ef3c7f48df 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
@@ -48,7 +48,7 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class V2ApiIntegrationTest extends SolrCloudTestCase {
-  private static String COLL_NAME = "collection1";
+  private static final String COLL_NAME = "collection1";
 
   @BeforeClass
   public static void createCluster() throws Exception {
@@ -80,11 +80,7 @@ public class V2ApiIntegrationTest extends SolrCloudTestCase {
             .build();
     v2Request.setResponseParser(responseParser);
     RemoteSolrException ex =
-        expectThrows(
-            RemoteSolrException.class,
-            () -> {
-              v2Request.process(cluster.getSolrClient());
-            });
+        expectThrows(RemoteSolrException.class, () -> 
v2Request.process(cluster.getSolrClient()));
     assertEquals(expectedCode, ex.code());
   }
 
@@ -114,28 +110,29 @@ public class V2ApiIntegrationTest extends 
SolrCloudTestCase {
   }
 
   @Test
-  public void testWTParam() throws Exception {
+  public void testInvalidWTParamReturnsError() throws Exception {
     V2Request request = new V2Request.Builder("/c/" + COLL_NAME + 
"/get/_introspect").build();
-    // TODO: If possible do this in a better way
+    // Using an invalid wt parameter should return a 400 error
     request.setResponseParser(new InputStreamResponseParser("bleh"));
     NamedList<Object> res = cluster.getSolrClient().request(request);
     String respString = InputStreamResponseParser.consumeResponseToString(res);
 
-    assertFalse(respString.contains("<body><h2>HTTP ERROR 500</h2>"));
-    assertFalse(respString.contains("500"));
-    assertFalse(respString.contains("NullPointerException"));
-    assertFalse(
-        respString.contains(
-            "<p>Problem accessing /solr/____v2/c/collection1/get/_introspect. 
Reason:"));
-    // since no-op response writer is used, doing contains match
-    assertTrue(respString.contains("/c/collection1/get"));
+    // Should get a 400 Bad Request error for unknown writer type
+    assertTrue(
+        "Expected error message about unknown writer type",
+        respString.contains("Unknown response writer type"));
+    assertTrue("Expected 400 error code", respString.contains("400"));
+  }
 
-    // no response parser
+  @Test
+  public void testWTParam() throws Exception {
+    // When no response parser is set, the default JSON writer should be used
+    V2Request request = new V2Request.Builder("/c/" + COLL_NAME + 
"/get/_introspect").build();
     request.setResponseParser(null);
     Map<?, ?> resp = resAsMap(cluster.getSolrClient(), request);
-    respString = resp.toString();
+    String respString = resp.toString();
 
-    assertFalse(respString.contains("<body><h2>HTTP ERROR 500</h2>"));
+    assertFalse(respString.contains("400"));
     assertFalse(
         respString.contains(
             "<p>Problem accessing /solr/____v2/c/collection1/get/_introspect. 
Reason:"));
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java 
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
index 530a2ba6009..a8250a1ba3a 100644
--- 
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
+++ 
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
@@ -194,6 +194,7 @@ public class TestPrometheusResponseWriter extends 
SolrTestCaseJ4 {
 
     try (SolrClient adminClient = 
getHttpSolrClient(solrTestRule.getBaseUrl())) {
       NamedList<Object> res = adminClient.request(req);
+      // Unknown wt parameter should return a 400 error
       assertEquals(400, res.get("responseStatus"));
     }
   }
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java 
b/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java
index 7b822b18848..34e006b727b 100644
--- a/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java
+++ b/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java
@@ -56,7 +56,9 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 {
     // we spin up.
     initCore("solrconfig.xml", "schema.xml");
 
-    writerNoBase = newRawResponseWriter(null); /* defaults to standard writer 
as base */
+    writerNoBase =
+        newRawResponseWriter(
+            null); /* null base uses core's default writer (XML for this 
core), or JSON if no core */
     writerXmlBase = newRawResponseWriter("xml");
     writerJsonBase = newRawResponseWriter("json");
     writerBinBase = newRawResponseWriter("javabin");
@@ -120,7 +122,7 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 {
       // we should have UTF-8 Bytes if we use an OutputStream
       ByteArrayOutputStream bout = new ByteArrayOutputStream();
       writer.write(bout, req(), rsp);
-      assertEquals(data, bout.toString(StandardCharsets.UTF_8.toString()));
+      assertEquals(data, bout.toString(StandardCharsets.UTF_8));
     }
   }
 
@@ -153,13 +155,12 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 
{
     assertEquals(xml, writerXmlBase.writeToString(req(), rsp));
     ByteArrayOutputStream xmlBout = new ByteArrayOutputStream();
     writerXmlBase.write(xmlBout, req(), rsp);
-    assertEquals(xml, xmlBout.toString(StandardCharsets.UTF_8.toString()));
+    assertEquals(xml, xmlBout.toString(StandardCharsets.UTF_8));
 
-    //
     assertEquals(xml, writerNoBase.writeToString(req(), rsp));
     ByteArrayOutputStream noneBout = new ByteArrayOutputStream();
     writerNoBase.write(noneBout, req(), rsp);
-    assertEquals(xml, noneBout.toString(StandardCharsets.UTF_8.toString()));
+    assertEquals(xml, noneBout.toString(StandardCharsets.UTF_8));
 
     // json
     String json = "{\n" + "  \"content\":\"test\",\n" + "  \"foo\":\"bar\"}\n";
@@ -206,7 +207,7 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 {
   }
 
   /**
-   * Generates a new {@link RawResponseWriter} wrapping the specified 
baseWriter name (which much
+   * Generates a new {@link RawResponseWriter} wrapping the specified 
baseWriter name (which must
    * either be an implicitly defined response writer, or one explicitly 
configured in
    * solrconfig.xml)
    *
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java 
b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
index 695ad0e1278..0ab46734b38 100644
--- 
a/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
+++ 
b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
@@ -31,34 +31,19 @@ public class TestResponseWritersRegistry extends 
SolrTestCaseJ4 {
 
   @Test
   public void testBuiltInWriterFallbackBehavior() {
-    QueryResponseWriter standardWriter = 
ResponseWritersRegistry.getWriter("standard");
+    QueryResponseWriter defaultWriter = 
ResponseWritersRegistry.getWriter("json");
 
     // 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));
+    assertThat("null writer should be same as default", nullWriter, 
is(defaultWriter));
 
     // 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));
+    assertThat("empty writer should be same as default", emptyWriter, 
is(defaultWriter));
 
     // 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));
+    assertThat("unknown writer should be null", unknownWriter, 
is(nullValue()));
   }
 }
diff --git a/solr/packaging/test/test_rolling_upgrade.bats 
b/solr/packaging/test/test_rolling_upgrade.bats
new file mode 100644
index 00000000000..ae956a868ba
--- /dev/null
+++ b/solr/packaging/test/test_rolling_upgrade.bats
@@ -0,0 +1,172 @@
+#!/usr/bin/env bats
+
+# 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.
+
+load bats_helper
+
+# You can test alternative images via 
+# export SOLR_BEGIN_IMAGE="apache/solr-nightly:9.9.0-slim" and then running
+# ./gradlew iTest --tests test_rolling_upgrade.bats
+SOLR_BEGIN_IMAGE="${SOLR_BEGIN_IMAGE:-apache/solr-nightly:9.10.0-SNAPSHOT-slim}"
+SOLR_END_IMAGE="${SOLR_END_IMAGE:-apache/solr-nightly:10.0.0-SNAPSHOT-slim}"
+
+setup() {
+  common_clean_setup
+
+  # Pre-checks
+  if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then
+    skip "Docker is not available"
+  fi
+  docker pull "$SOLR_BEGIN_IMAGE" || skip "Docker image $SOLR_BEGIN_IMAGE is 
not available"
+  docker pull "$SOLR_END_IMAGE" || skip "Docker image $SOLR_END_IMAGE is not 
available"
+
+  # Record test start time for scoping logs on failure
+  TEST_STARTED_AT_ISO=$(date -Iseconds)
+  export TEST_STARTED_AT_ISO
+
+  # Persist artifacts under Gradle’s test-output
+  ARTIFACT_DIR="${TEST_OUTPUT_DIR}/docker"
+  mkdir -p "$ARTIFACT_DIR"
+  export ARTIFACT_DIR
+}
+
+teardown() {
+  failed=$([[ -z "${BATS_TEST_COMPLETED:-}" ]] && [[ -z 
"${BATS_TEST_SKIPPED:-}" ]] && echo 1 || echo 0)
+  if [[ "$failed" -eq 1 ]]; then
+    echo "# Test failed - capturing Docker diagnostics" >&3
+    echo "# === docker ps (summary) ===" >&3
+    docker ps -a --format 'table 
{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Ports}}' >&3 2>&3 || true
+  fi
+
+  for container in solr-node1 solr-node2 solr-node3; do
+    if docker ps -a --format '{{.Names}}' | grep -q "^${container}$" 
2>/dev/null; then
+      if [[ "$failed" -eq 1 ]]; then
+        echo "# === Docker logs for $container ===" >&3
+        docker logs --timestamps --since "$TEST_STARTED_AT_ISO" "$container" 
>&3 2>&3 || echo "# Failed to get logs for $container" >&3
+        echo "# === Docker inspect for $container ===" >&3
+        docker inspect "$container" | jq '.[] | {Name: .Name, State: .State, 
Ports: .NetworkSettings.Ports}' >&3 2>&3 || true
+      fi
+      # Persist artifacts
+      docker logs --timestamps "$container" >"$ARTIFACT_DIR/${container}.log" 
2>&1 || true
+      docker inspect "$container" >"$ARTIFACT_DIR/${container}.inspect.json" 
2>&1 || true
+      docker exec "$container" ps aux >"$ARTIFACT_DIR/${container}.ps.txt" 
2>&1 || true
+    fi
+  done
+
+  echo "# Docker artifacts saved to: $ARTIFACT_DIR" >&3
+
+  docker stop solr-node1 solr-node2 solr-node3 2>/dev/null || true
+  docker rm solr-node1 solr-node2 solr-node3 2>/dev/null || true
+  docker volume rm solr-data1 solr-data2 solr-data3 2>/dev/null || true
+  docker network rm solrcloud-test 2>/dev/null || true
+}
+
+@test "Docker SolrCloud rolling upgrade" {
+  # Networking & volumes
+  docker network create solrcloud-test
+  docker volume create solr-data1
+  docker volume create solr-data2
+  docker volume create solr-data3
+
+  echo "Starting solr-node1 with embedded ZooKeeper"
+  docker run --name solr-node1 -d \
+    --network solrcloud-test \
+    --memory=400m \
+    --platform linux/amd64 \
+    -v solr-data1:/var/solr \
+    "$SOLR_BEGIN_IMAGE" solr start -f -c -m 200m --host solr-node1 -p 8983
+  docker exec solr-node1 solr assert --started http://solr-node1:8983 
--timeout 10000
+
+  # start next 2 in parallel
+
+  echo "Starting solr-node2 connected to first node's ZooKeeper"
+  docker run --name solr-node2 -d \
+    --network solrcloud-test \
+    --memory=400m \
+    --platform linux/amd64 \
+    -v solr-data2:/var/solr \
+    "$SOLR_BEGIN_IMAGE" solr start -f -c -m 200m --host solr-node2 -p 8984 -z 
solr-node1:9983
+
+  echo "Started solr-node3 connected to first node's ZooKeeper"
+  docker run --name solr-node3 -d \
+    --network solrcloud-test \
+    --memory=400m \
+    --platform linux/amd64 \
+    -v solr-data3:/var/solr \
+    "$SOLR_BEGIN_IMAGE" solr start -f -c -m 200m --host solr-node3 -p 8985 -z 
solr-node1:9983
+
+  docker exec solr-node2 solr assert --started http://solr-node2:8984 
--timeout 30000
+  docker exec solr-node3 solr assert --started http://solr-node3:8985 
--timeout 30000
+
+  echo "Creating a Collection"
+  docker exec --user=solr solr-node1 solr create -c test-collection -n 
techproducts --shards 3
+
+  echo "Checking collection health"
+  wait_for 30 1 docker exec solr-node1 solr healthcheck -c test-collection
+
+  echo "Add some sample data"
+  docker exec --user=solr solr-node1 solr post -c test-collection 
example/exampledocs/mem.xml
+  assert_success
+
+  # Begin rolling upgrade - upgrade node 3 first (reverse order: 3, 2, 1)
+  echo "Starting rolling upgrade - upgrading node 3"
+  docker stop solr-node3
+  docker rm solr-node3
+  docker run --name solr-node3 -d \
+    --network solrcloud-test \
+    --memory=400m \
+    --platform linux/amd64 \
+    -v solr-data3:/var/solr \
+    "$SOLR_END_IMAGE" solr start -f -m 200m --host solr-node3 -p 8985 -z 
solr-node1:9983
+  docker exec solr-node3 solr assert --started http://solr-node3:8985 
--timeout 30000
+  assert_success
+
+  # Upgrade node 2 second
+  echo "Upgrading node 2"
+  docker stop solr-node2
+  docker rm solr-node2
+  docker run --name solr-node2 -d \
+    --network solrcloud-test \
+    --memory=400m \
+    --platform linux/amd64 \
+    -v solr-data2:/var/solr \
+    "$SOLR_END_IMAGE" solr start -f -m 200m --host solr-node2 -p 8984 -z 
solr-node1:9983
+  docker exec solr-node2 solr assert --started http://solr-node2:8984 
--timeout 30000
+  assert_success
+
+  echo "Upgrading node 1 (ZK node)"
+  docker stop solr-node1
+  docker rm solr-node1
+  docker run --name solr-node1 -d \
+    --network solrcloud-test \
+    --memory=400m \
+    --platform linux/amd64 \
+    -v solr-data1:/var/solr \
+    "$SOLR_END_IMAGE" solr start -f -m 200m --host solr-node1 -p 8983
+  docker exec solr-node1 solr assert --started http://solr-node1:8983 
--timeout 30000
+  assert_success
+
+  # Final collection health check
+  wait_for 30 1 docker exec solr-node1 solr healthcheck -c test-collection
+
+  echo "checking cluster has exactly 3 live nodes"
+  run docker exec solr-node1 curl -s 
"http://solr-node1:8983/solr/admin/collections?action=CLUSTERSTATUS";
+  assert_success
+  
+  local live_nodes_count=$(echo "$output" | jq -r '.cluster.live_nodes | 
length')
+  assert_equal "$live_nodes_count" "3"
+
+}
diff --git a/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java 
b/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java
index 29caa318a04..a933605466f 100644
--- a/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java
+++ b/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java
@@ -419,6 +419,7 @@ public class TestHarness extends BaseTestHarness {
     @SuppressWarnings({"unchecked"})
     public LocalSolrQueryRequest makeRequest(String... q) {
       if (q.length == 1) {
+        args.computeIfAbsent("wt", k -> "xml");
         return new LocalSolrQueryRequest(
             TestHarness.this.getCore(), q[0], qtype, start, limit, args);
       }


Reply via email to