You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ep...@apache.org on 2026/02/10 17:20:34 UTC
(solr) branch branch_10x updated (27bda40a219 -> a726e0bfed6)
This is an automated email from the ASF dual-hosted git repository.
epugh pushed a change to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
from 27bda40a219 Need to add "raw" as a node/container level request writer (#4123)
new a7f40683e60 SOLR-18085: Remove use of "wt=standard" internally to Solr. (#4080)
new a726e0bfed6 SOLR-18085: Remove use of "wt=standard" internally to Solr. (#4080)
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../SOLR-15349.yml => unreleased/SOLR-18085.yml} | 6 +-
.../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, 300 insertions(+), 147 deletions(-)
copy changelog/{v10.0.0/SOLR-15349.yml => unreleased/SOLR-18085.yml} (57%)
create mode 100644 solr/packaging/test/test_rolling_upgrade.bats
(solr) 02/02: SOLR-18085: Remove use of "wt=standard" internally to Solr. (#4080)
Posted by ep...@apache.org.
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 a726e0bfed620844acf339ce6a34d684c7656a42
Author: Eric Pugh <ep...@opensourceconnections.com>
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)
(solr) 01/02: SOLR-18085: Remove use of "wt=standard" internally to Solr. (#4080)
Posted by ep...@apache.org.
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 <ep...@opensourceconnections.com>
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);
}