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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog/unreleased/PR#4526-HttpSolrClient-error-message.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: >
HttpSolrClient: made error messages more consistent, avoiding complaining about the content-type
when it's text/html
type: changed
authors:
- name: David Smiley
links:
- name: PR#4526
url: https://github.com/apache/solr/pull/4526
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import jakarta.servlet.http.HttpServletResponse;
import java.lang.invoke.MethodHandles;
import java.util.Map;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.RemoteSolrException;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.SolrQuery;
Expand All @@ -43,9 +43,6 @@ public class TestAuthenticationFramework extends SolrCloudTestCase {
private static final String configName = "solrCloudCollectionConfig";
private static final String collectionName = "testcollection";

static String requestUsername = MockAuthenticationPlugin.expectedUsername;
static String requestPassword = MockAuthenticationPlugin.expectedPassword;

@Override
public void setUp() throws Exception {
setupAuthenticationPlugin();
Expand All @@ -70,11 +67,9 @@ public void testBasics() throws Exception {

// Should fail with 401
try {
SolrServerException e =
expectThrows(SolrServerException.class, this::collectionCreateSearchDeleteTwice);
assertTrue(
"Should've returned a 401 error",
e.getMessage().contains("401") || e.getMessage().contains("Authentication"));
RemoteSolrException e =
expectThrows(RemoteSolrException.class, this::collectionCreateSearchDeleteTwice);
assertEquals(401, e.code()); // Authentication failed
} finally {
MockAuthenticationPlugin.expectedUsername = null;
MockAuthenticationPlugin.expectedPassword = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ public void testBasicAuth() throws Exception {
() -> {
new UpdateRequest().deleteByQuery("*:*").process(aNewClient, COLLECTION);
});
assertTrue(e.getMessage(), e.getMessage().contains("Authentication failed"));
assertEquals(401, e.code()); // Authentication failed
} finally {
aNewClient.close();
cluster.stopJettySolrRunner(aNewJetty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.RemoteSolrException;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.impl.ZkClientClusterStateProvider;
import org.apache.solr.client.solrj.jetty.CloudJettySolrClient;
import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
Expand All @@ -56,7 +58,6 @@
import org.junit.Test;
import org.mockito.Mockito;

@LuceneTestCase.AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-17810")
public class TestRequestRateLimiter extends SolrCloudTestCase {
private static final String FIRST_COLLECTION = "c1";
private static final String SECOND_COLLECTION = "c2";
Expand All @@ -69,7 +70,7 @@ public static void setupCluster() throws Exception {

@Test
public void testConcurrentQueries() throws Exception {
try (CloudSolrClient client = cluster.newSolrClient(FIRST_COLLECTION)) {
try (CloudSolrClient client = newHttp1SolrClient(FIRST_COLLECTION)) {

CollectionAdminRequest.createCollection(FIRST_COLLECTION, 1, 1).process(client);
cluster.waitForActiveCollection(FIRST_COLLECTION, 1, 1);
Expand Down Expand Up @@ -289,7 +290,7 @@ private static HttpServletRequest newRequest(String type, String ctx) {

@Nightly
public void testSlotBorrowing() throws Exception {
try (CloudSolrClient client = cluster.newSolrClient(SECOND_COLLECTION)) {
try (CloudSolrClient client = newHttp1SolrClient(SECOND_COLLECTION)) {

CollectionAdminRequest.createCollection(SECOND_COLLECTION, 1, 1).process(client);
cluster.waitForActiveCollection(SECOND_COLLECTION, 1, 1);
Expand Down Expand Up @@ -429,6 +430,14 @@ public SlotReservation allowSlotBorrowing() throws InterruptedException {
}
}

private CloudSolrClient newHttp1SolrClient(String collection) {
return new CloudJettySolrClient.Builder(
new ZkClientClusterStateProvider(cluster.getZkServer().getZkAddress()))
.withDefaultCollection(collection)
.withHttpClientBuilder(new HttpJettySolrClient.Builder().useHttp1_1(true))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude's root cause, and suggested this fix:

When 350 concurrent queries hit the rate limiter and get rejected as 429s, the server sends rapid RST_STREAM frames on the multiplexed HTTP/2 connection. This triggers HTTP/2's ENHANCE_YOUR_CALM protection, which tears down the entire connection. Subsequent requests then fail with IOException, which the CloudSolrClient sees as a communication error → "No live SolrServers."

The old Apache HTTP client used HTTP/1.1 (separate connections per request), so this never happened. The Jetty client defaults to HTTP/2.

.build();
}

private static class MockBuilder extends RateLimitManager.Builder {
private final RequestRateLimiter queryRequestRateLimiter;
private final RequestRateLimiter indexRequestRateLimiter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -188,11 +188,12 @@ protected NamedList<Object> processErrorsAndResponse(
String responseMethod,
final ResponseParser processor,
InputStream is,
String mimeType,
String mimeType, // aka contentType
String encoding,
final boolean isV2Api,
String urlExceptionMessage)
throws SolrServerException {
Objects.requireNonNull(processor);
boolean shouldClose = true;
try {
// handle some http level checks before trying to parse the response
Expand All @@ -209,7 +210,7 @@ protected NamedList<Object> processErrorsAndResponse(
}
break;
default:
if (processor == null || mimeType == null) {
if (mimeType == null || mimeType.equals("text/html")) {
Comment on lines -212 to +213

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the meat of the change.

throw new RemoteSolrException(
urlExceptionMessage,
httpStatus,
Expand All @@ -218,15 +219,15 @@ protected NamedList<Object> processErrorsAndResponse(
}
}

checkContentType(processor, is, mimeType, encoding, httpStatus, urlExceptionMessage);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moved this above wantStream / InputStreamResponseParser because I think it makes sense to check this fairly early. Someone could subclass ISRP and specify the content types that should be checked.


if (wantStream(processor)) {
// Only case where stream should not be closed
shouldClose = false;
// no processor specified, return raw stream
return InputStreamResponseParser.createInputStreamNamedList(httpStatus, is);
}

checkContentType(processor, is, mimeType, encoding, httpStatus, urlExceptionMessage);

NamedList<Object> rsp;
try {
rsp = processor.processResponse(is, encoding);
Expand All @@ -240,14 +241,11 @@ protected NamedList<Object> processErrorsAndResponse(
}
if (httpStatus != 200 && !isV2Api) {
if (error == null) {
StringBuilder msg =
new StringBuilder()
.append(responseReason)
.append("\n")
.append("request: ")
.append(responseMethod);
String reason = URLDecoder.decode(msg.toString(), FALLBACK_CHARSET);
throw new RemoteSolrException(urlExceptionMessage, httpStatus, reason, null);
throw new RemoteSolrException(
urlExceptionMessage,
httpStatus,
"non ok status: " + httpStatus + ", message:" + responseReason,
null);
Comment on lines -243 to +248

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@HoustonPutman , you added this in SOLR-17998 #1657 I didn't see much sense in this particular string so I changed it to match the message string above line 217

} else {
throw new RemoteSolrException(urlExceptionMessage, httpStatus, error);
}
Expand Down
Loading