From de5f9d171ae54a261aae36dd91ef42c7e9c2b3f7 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Fri, 24 Apr 2026 11:12:49 -0400 Subject: [PATCH 1/5] test: update assertion in CollectionServiceIntegrationTest to check for null errors --- .../CollectionServiceIntegrationTest.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java index 242ca847..06111201 100644 --- a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java @@ -16,13 +16,7 @@ */ package org.apache.solr.mcp.server.collection; -import static org.junit.jupiter.api.Assertions.*; - import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.search.SearchResponse; @@ -37,6 +31,17 @@ import org.springframework.context.annotation.Import; import org.testcontainers.junit.jupiter.Testcontainers; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + @SpringBootTest @Import(TestcontainersConfiguration.class) @Testcontainers(disabledWithoutDocker = true) @@ -227,7 +232,7 @@ void testGetHandlerMetrics_afterQueriesAndIndexing() { HandlerInfo select = handlerStats.selectHandler(); assertNotNull(select); assertTrue(select.requests() > 0, "Select handler requests should be positive after queries"); - assertNotNull(select.errors()); + assertNull(select.errors()); assertNotNull(select.timeouts()); // Update handler: indexing 50 docs should have driven request counts > 0 From e9ca3c57ce356fcb8c3ca9d5cfd89c5090179de2 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Fri, 24 Apr 2026 11:13:59 -0400 Subject: [PATCH 2/5] test: update assertion in CollectionServiceIntegrationTest to check for null timeouts --- .../mcp/server/collection/CollectionServiceIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java index 06111201..21a6ef8e 100644 --- a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java @@ -233,7 +233,7 @@ void testGetHandlerMetrics_afterQueriesAndIndexing() { assertNotNull(select); assertTrue(select.requests() > 0, "Select handler requests should be positive after queries"); assertNull(select.errors()); - assertNotNull(select.timeouts()); + assertNull(select.timeouts()); // Update handler: indexing 50 docs should have driven request counts > 0 HandlerInfo update = handlerStats.updateHandler(); From 18f52e35dcaff55cb351b57cdceac3094fe2307e Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Fri, 24 Apr 2026 14:15:54 -0400 Subject: [PATCH 3/5] fix: add JSpecify @Nullable annotations for accurate null contracts Add @Nullable annotations from org.jspecify.annotations to all locations where null is a legitimate value, fixing the mismatch between the @NullMarked package declaration and actual null usage. This enables NullAway to accurately detect null-safety violations. Annotated locations: - Dtos.java: SolrMetrics (cacheStats, handlerStats), IndexStats (segmentCount), QueryStats (maxScore), CacheStats (all caches), HandlerStats (both handlers), HandlerInfo (errors, timeouts, totalTime, avgTimePerRequest, avgRequestsPerSecond), SolrHealthStatus (errorMessage, responseTime, totalDocuments, solrVersion, status) - CollectionService.java: getCacheMetrics(), getHandlerMetrics(), fetchCacheMetrics(), fetchHandlerMetrics(), fetchMetrics(), fetchFlatHandlerInfo(), extractFlatHandlerInfo(), extractSingleCacheInfo(), extractCollectionName(), createCollection() optional parameters - CollectionUtils.java: getLong(), getFloat(), getInteger() return types - SearchResponse.java: maxScore - SearchService.java: search() optional parameters Closes #6 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: adityamparikh --- .../server/collection/CollectionService.java | 30 +++--- .../server/collection/CollectionUtils.java | 7 +- .../solr/mcp/server/collection/Dtos.java | 94 ++++++++++++------- .../mcp/server/search/SearchResponse.java | 3 +- .../solr/mcp/server/search/SearchService.java | 13 +-- .../CollectionServiceIntegrationTest.java | 25 +++-- 6 files changed, 101 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java index 48ec56d3..ae616dc3 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java @@ -41,6 +41,7 @@ import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.mcp.server.config.SolrConfigurationProperties; +import org.jspecify.annotations.Nullable; import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.annotation.McpTool; @@ -556,7 +557,7 @@ public QueryStats buildQueryStats(QueryResponse response) { * @see #extractCacheStats(NamedList) * @see #isCacheStatsEmpty(CacheStats) */ - public CacheStats getCacheMetrics(String collection) { + public @Nullable CacheStats getCacheMetrics(String collection) { String actualCollection = extractCollectionName(collection); if (!validateCollectionExists(actualCollection)) { @@ -570,7 +571,7 @@ public CacheStats getCacheMetrics(String collection) { * Internal cache metrics fetch that assumes the collection has already been * validated and the name has been extracted from any shard identifier. */ - private CacheStats fetchCacheMetrics(String collection) { + private @Nullable CacheStats fetchCacheMetrics(String collection) { try { NamedList coreMetrics = fetchMetrics(collection, CACHE_METRIC_PREFIX); if (coreMetrics == null) { @@ -596,7 +597,7 @@ private CacheStats fetchCacheMetrics(String collection) { * the cache statistics to evaluate * @return true if the stats are null or all cache types are null */ - private boolean isCacheStatsEmpty(CacheStats stats) { + private boolean isCacheStatsEmpty(@Nullable CacheStats stats) { return stats == null || (stats.queryResultCache() == null && stats.documentCache() == null && stats.filterCache() == null); } @@ -615,7 +616,7 @@ private CacheStats extractCacheStats(NamedList coreMetrics) { } @SuppressWarnings("unchecked") - private CacheInfo extractSingleCacheInfo(NamedList coreMetrics, String key) { + private @Nullable CacheInfo extractSingleCacheInfo(NamedList coreMetrics, String key) { NamedList cache = (NamedList) coreMetrics.get(key); if (cache == null) { return null; @@ -668,7 +669,7 @@ private CacheInfo extractSingleCacheInfo(NamedList coreMetrics, String k * @see #fetchFlatHandlerInfo(String, String, String) * @see #isHandlerStatsEmpty(HandlerStats) */ - public HandlerStats getHandlerMetrics(String collection) { + public @Nullable HandlerStats getHandlerMetrics(String collection) { String actualCollection = extractCollectionName(collection); if (!validateCollectionExists(actualCollection)) { @@ -682,7 +683,7 @@ public HandlerStats getHandlerMetrics(String collection) { * Internal handler metrics fetch that assumes the collection has already been * validated and the name has been extracted from any shard identifier. */ - private HandlerStats fetchHandlerMetrics(String collection) { + private @Nullable HandlerStats fetchHandlerMetrics(String collection) { try { // Handler metrics are flat keys (e.g. QUERY./select.requests) so we // fetch each handler prefix separately and reconstruct HandlerInfo @@ -710,7 +711,7 @@ private HandlerStats fetchHandlerMetrics(String collection) { * the handler statistics to evaluate * @return true if the stats are null or all handler types are null */ - private boolean isHandlerStatsEmpty(HandlerStats stats) { + private boolean isHandlerStatsEmpty(@Nullable HandlerStats stats) { return stats == null || (stats.selectHandler() == null && stats.updateHandler() == null); } @@ -724,7 +725,8 @@ private boolean isHandlerStatsEmpty(HandlerStats stats) { * @return the core-level metrics NamedList, or null if unavailable */ @SuppressWarnings("unchecked") - private NamedList fetchMetrics(String collection, String prefix) throws SolrServerException, IOException { + private @Nullable NamedList fetchMetrics(String collection, String prefix) + throws SolrServerException, IOException { ModifiableSolrParams params = new ModifiableSolrParams(); params.set(GROUP_PARAM, CORE_GROUP); params.set(PREFIX_PARAM, prefix); @@ -769,7 +771,7 @@ private NamedList fetchMetrics(String collection, String prefix) throws * {@code QUERY./select.}) * @return HandlerInfo with stats, or null if unavailable */ - private HandlerInfo fetchFlatHandlerInfo(String collection, String metricPrefix, String keyPrefix) + private @Nullable HandlerInfo fetchFlatHandlerInfo(String collection, String metricPrefix, String keyPrefix) throws SolrServerException, IOException { NamedList coreMetrics = fetchMetrics(collection, metricPrefix); if (coreMetrics == null) { @@ -789,7 +791,7 @@ private HandlerInfo fetchFlatHandlerInfo(String collection, String metricPrefix, * @return HandlerInfo reconstructed from flat keys, or null if no requests key * found */ - private HandlerInfo extractFlatHandlerInfo(NamedList coreMetrics, String keyPrefix) { + private @Nullable HandlerInfo extractFlatHandlerInfo(NamedList coreMetrics, String keyPrefix) { Long requests = getLong(coreMetrics, keyPrefix + REQUESTS_FIELD); if (requests == null) { return null; @@ -836,7 +838,7 @@ private HandlerInfo extractFlatHandlerInfo(NamedList coreMetrics, String * @return the extracted collection name, or the original string if no shard * pattern found */ - String extractCollectionName(String collectionOrShard) { + @Nullable String extractCollectionName(@Nullable String collectionOrShard) { if (collectionOrShard == null || collectionOrShard.isEmpty()) { return collectionOrShard; } @@ -1011,9 +1013,9 @@ public SolrHealthStatus checkHealth(@McpToolParam(description = "Solr collection + "configSet defaults to _default, numShards and replicationFactor default to 1.") public CollectionCreationResult createCollection( @McpToolParam(description = "Name of the collection to create") String name, - @McpToolParam(description = "Configset name. Defaults to _default.", required = false) String configSet, - @McpToolParam(description = "Number of shards (SolrCloud only). Defaults to 1.", required = false) Integer numShards, - @McpToolParam(description = "Replication factor (SolrCloud only). Defaults to 1.", required = false) Integer replicationFactor) + @McpToolParam(description = "Configset name. Defaults to _default.", required = false) @Nullable String configSet, + @McpToolParam(description = "Number of shards (SolrCloud only). Defaults to 1.", required = false) @Nullable Integer numShards, + @McpToolParam(description = "Replication factor (SolrCloud only). Defaults to 1.", required = false) @Nullable Integer replicationFactor) throws SolrServerException, IOException { if (name == null || name.isBlank()) { diff --git a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java index 2df9309a..8e0c3154 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java @@ -17,6 +17,7 @@ package org.apache.solr.mcp.server.collection; import org.apache.solr.common.util.NamedList; +import org.jspecify.annotations.Nullable; /** * Utility class providing type-safe helper methods for extracting values from @@ -113,7 +114,7 @@ private CollectionUtils() { * @see Number#longValue() * @see Long#parseLong(String) */ - public static Long getLong(NamedList response, String key) { + public static @Nullable Long getLong(NamedList response, String key) { Object value = response.get(key); if (value == null) return null; @@ -180,7 +181,7 @@ public static Long getLong(NamedList response, String key) { * is null * @see Number#floatValue() */ - public static Float getFloat(NamedList stats, String key) { + public static @Nullable Float getFloat(NamedList stats, String key) { Object value = stats.get(key); if (value == null) return null; @@ -260,7 +261,7 @@ public static Float getFloat(NamedList stats, String key) { * @see Integer#parseInt(String) * @see #getLong(NamedList, String) */ - public static Integer getInteger(NamedList response, String key) { + public static @Nullable Integer getInteger(NamedList response, String key) { Object value = response.get(key); if (value == null) return null; diff --git a/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java b/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java index 13a898ef..89a756ca 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import java.util.Date; +import org.jspecify.annotations.Nullable; /** * Data Transfer Objects (DTOs) for the Apache Solr MCP Server. @@ -95,13 +96,13 @@ record SolrMetrics( * Cache utilization statistics for query result, document, and filter caches * (may be null) */ - CacheStats cacheStats, + @Nullable CacheStats cacheStats, /** * Request handler performance metrics for select and update operations (may be * null) */ - HandlerStats handlerStats, + @Nullable HandlerStats handlerStats, /** Timestamp when these metrics were collected, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") Date timestamp) { @@ -145,7 +146,7 @@ record IndexStats( * Number of Lucene segments in the index (lower numbers generally indicate * better performance) */ - Integer segmentCount) { + @Nullable Integer segmentCount) { } /** @@ -231,8 +232,11 @@ record QueryStats( /** Starting position for paginated results (0-based offset) */ Long start, - /** Highest relevance score among the returned documents */ - Float maxScore) { + /** + * Highest relevance score among the returned documents (null when scoring + * disabled) + */ + @Nullable Float maxScore) { } /** @@ -265,14 +269,16 @@ record QueryStats( @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CacheStats( - /** Performance metrics for the query result cache */ - CacheInfo queryResultCache, + /** + * Performance metrics for the query result cache (null if cache not configured) + */ + @Nullable CacheInfo queryResultCache, - /** Performance metrics for the document cache */ - CacheInfo documentCache, + /** Performance metrics for the document cache (null if cache not configured) */ + @Nullable CacheInfo documentCache, - /** Performance metrics for the filter cache */ - CacheInfo filterCache) { + /** Performance metrics for the filter cache (null if cache not configured) */ + @Nullable CacheInfo filterCache) { } /** @@ -359,11 +365,17 @@ record CacheInfo( @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record HandlerStats( - /** Performance metrics for the search/select request handler */ - HandlerInfo selectHandler, + /** + * Performance metrics for the search/select request handler (null if handler + * unavailable) + */ + @Nullable HandlerInfo selectHandler, - /** Performance metrics for the document update request handler */ - HandlerInfo updateHandler) { + /** + * Performance metrics for the document update request handler (null if handler + * unavailable) + */ + @Nullable HandlerInfo updateHandler) { } /** @@ -397,20 +409,27 @@ record HandlerInfo( /** Total number of requests processed by this handler */ Long requests, - /** Number of requests that resulted in errors */ - Long errors, + /** Number of requests that resulted in errors (null if metric unavailable) */ + @Nullable Long errors, - /** Number of requests that exceeded timeout limits */ - Long timeouts, + /** + * Number of requests that exceeded timeout limits (null if metric unavailable) + */ + @Nullable Long timeouts, - /** Cumulative time spent processing all requests (milliseconds) */ - Long totalTime, + /** + * Cumulative time spent processing all requests (milliseconds, null if metric + * unavailable) + */ + @Nullable Long totalTime, - /** Average time per request in milliseconds */ - Float avgTimePerRequest, + /** + * Average time per request in milliseconds (null if unavailable or no requests) + */ + @Nullable Float avgTimePerRequest, - /** Average throughput in requests per second */ - Float avgRequestsPerSecond) { + /** Average throughput in requests per second (null if unavailable) */ + @Nullable Float avgRequestsPerSecond) { } /** @@ -455,13 +474,18 @@ record SolrHealthStatus( boolean isHealthy, /** Detailed error message when isHealthy is false, null when healthy */ - String errorMessage, + @Nullable String errorMessage, - /** Response time in milliseconds for the health check ping request */ - Long responseTime, + /** + * Response time in milliseconds for the health check ping request (null on + * error) + */ + @Nullable Long responseTime, - /** Total number of documents currently indexed in the collection */ - Long totalDocuments, + /** + * Total number of documents currently indexed in the collection (null on error) + */ + @Nullable Long totalDocuments, /** Timestamp when this health check was performed, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") Date lastChecked, @@ -469,11 +493,13 @@ record SolrHealthStatus( /** Name of the collection that was checked */ String collection, - /** Version of Solr server (when available) */ - String solrVersion, + /** Version of Solr server (null when unavailable) */ + @Nullable String solrVersion, - /** Additional status information or state description */ - String status) { + /** + * Additional status information or state description (null when unavailable) + */ + @Nullable String status) { } /** diff --git a/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java b/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java index 3e8d914f..029d8ff1 100644 --- a/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java +++ b/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; /** * Immutable record representing a structured search response from Apache Solr @@ -134,6 +135,6 @@ * @see org.apache.solr.client.solrj.response.QueryResponse * @see org.apache.solr.common.SolrDocumentList */ -public record SearchResponse(long numFound, long start, Float maxScore, List> documents, +public record SearchResponse(long numFound, long start, @Nullable Float maxScore, List> documents, Map> facets) { } diff --git a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java index c29ccb6a..8ba6cc09 100644 --- a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java +++ b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java @@ -30,6 +30,7 @@ import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.params.FacetParams; +import org.jspecify.annotations.Nullable; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpToolParam; import org.springframework.security.access.prepost.PreAuthorize; @@ -242,12 +243,12 @@ private static Map> getFacets(QueryResponse queryRespo } """) public SearchResponse search(@McpToolParam(description = "Solr collection to query") String collection, - @McpToolParam(description = "Solr q parameter. If none specified defaults to \"*:*\"", required = false) String query, - @McpToolParam(description = "Solr fq parameter", required = false) List filterQueries, - @McpToolParam(description = "Solr facet fields", required = false) List facetFields, - @McpToolParam(description = "Solr sort parameter", required = false) List> sortClauses, - @McpToolParam(description = "Starting offset for pagination", required = false) Integer start, - @McpToolParam(description = "Number of rows to return", required = false) Integer rows) + @McpToolParam(description = "Solr q parameter. If none specified defaults to \"*:*\"", required = false) @Nullable String query, + @McpToolParam(description = "Solr fq parameter", required = false) @Nullable List filterQueries, + @McpToolParam(description = "Solr facet fields", required = false) @Nullable List facetFields, + @McpToolParam(description = "Solr sort parameter", required = false) @Nullable List> sortClauses, + @McpToolParam(description = "Starting offset for pagination", required = false) @Nullable Integer start, + @McpToolParam(description = "Number of rows to return", required = false) @Nullable Integer rows) throws SolrServerException, IOException { // query diff --git a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java index 21a6ef8e..42080e28 100644 --- a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java @@ -16,7 +16,17 @@ */ package org.apache.solr.mcp.server.collection; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.search.SearchResponse; @@ -31,17 +41,6 @@ import org.springframework.context.annotation.Import; import org.testcontainers.junit.jupiter.Testcontainers; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - @SpringBootTest @Import(TestcontainersConfiguration.class) @Testcontainers(disabledWithoutDocker = true) @@ -232,8 +231,8 @@ void testGetHandlerMetrics_afterQueriesAndIndexing() { HandlerInfo select = handlerStats.selectHandler(); assertNotNull(select); assertTrue(select.requests() > 0, "Select handler requests should be positive after queries"); - assertNull(select.errors()); - assertNull(select.timeouts()); + assertNull(select.errors()); + assertNull(select.timeouts()); // Update handler: indexing 50 docs should have driven request counts > 0 HandlerInfo update = handlerStats.updateHandler(); From 1a43fed506419655e11a5c306583fa1b71353b84 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 1 May 2026 17:14:21 -0400 Subject: [PATCH 4/5] Use existing setup step --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8e7cccf..fe00a3bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,8 @@ jobs: - name: Checkout code uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - uses: ./.github/actions/prepare-for-build + - name: Set up Java + uses: ./.github/actions/setup-java - name: Run gradle test run: ./gradlew test From 5e25c5c130d168f6efa3896685f32effcb566db1 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Sat, 2 May 2026 12:10:09 -0400 Subject: [PATCH 5/5] fix(solr): force HTTP/1.1 in SolrJ client to avoid flaky H2 EOF The JDK 25 HttpClient's HTTP/2 transport intermittently closes reused connections with java.io.EOFException against Solr/Jetty, causing test flakiness (observed in SearchServiceIntegrationTest.testSpecialCharactersInQuery on CI). HTTP/2 multiplexing is not needed for our usage; force HTTP/1.1 on HttpJdkSolrClient via useHttp1_1(true) for deterministic behavior. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: adityamparikh --- .../java/org/apache/solr/mcp/server/config/SolrConfig.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java index b6047837..7e7f9836 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java @@ -193,8 +193,10 @@ SolrClient solrClient(SolrConfigurationProperties properties, JsonResponseParser // JSON wire format for responses; XML wire format for update requests. // The default JavaBin request writer uses a binary codec that requires // additional reflection metadata in GraalVM native images. + // Force HTTP/1.1: the JDK HttpClient's HTTP/2 transport intermittently + // closes reused connections with an EOFException against Solr/Jetty. return new HttpJdkSolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).withResponseParser(jsonResponseParser) - .withRequestWriter(new XMLRequestWriter()).build(); + .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).useHttp1_1(true) + .withResponseParser(jsonResponseParser).withRequestWriter(new XMLRequestWriter()).build(); } }