Browse Source

Add level parameter validation in REST layer (#94136)

Relates #93981
Howard 2 years ago
parent
commit
fc9c0cefd9
21 changed files with 303 additions and 72 deletions
  1. 5 0
      docs/changelog/94136.yaml
  2. 40 0
      server/src/main/java/org/elasticsearch/action/ClusterStatsLevel.java
  3. 40 0
      server/src/main/java/org/elasticsearch/action/NodeStatsLevel.java
  4. 0 28
      server/src/main/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthRequest.java
  5. 3 2
      server/src/main/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponse.java
  6. 1 1
      server/src/main/java/org/elasticsearch/action/admin/cluster/node/stats/NodesStatsRequest.java
  7. 4 9
      server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java
  8. 3 1
      server/src/main/java/org/elasticsearch/cluster/health/ClusterIndexHealth.java
  9. 6 10
      server/src/main/java/org/elasticsearch/indices/NodeIndicesStats.java
  10. 3 0
      server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterHealthAction.java
  11. 3 0
      server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesStatsAction.java
  12. 3 0
      server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsAction.java
  13. 3 2
      server/src/test/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponsesTests.java
  14. 4 4
      server/src/test/java/org/elasticsearch/cluster/health/ClusterIndexHealthTests.java
  15. 1 1
      server/src/test/java/org/elasticsearch/indices/NodeIndicesStatsTests.java
  16. 44 0
      server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestClusterHealthActionTests.java
  17. 40 0
      server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestNodesStatsActionTests.java
  18. 39 0
      server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsActionTests.java
  19. 4 9
      x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponse.java
  20. 6 5
      x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestSearchableSnapshotsStatsAction.java
  21. 51 0
      x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponseTests.java

+ 5 - 0
docs/changelog/94136.yaml

@@ -0,0 +1,5 @@
+pr: 94136
+summary: Add level parameter validation in REST layer 
+area: Infra/REST API
+type: bug
+issues: [93981]

+ 40 - 0
server/src/main/java/org/elasticsearch/action/ClusterStatsLevel.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action;
+
+import org.elasticsearch.xcontent.ToXContent;
+
+public enum ClusterStatsLevel {
+    CLUSTER("cluster"),
+    INDICES("indices"),
+    SHARDS("shards");
+
+    private final String level;
+
+    ClusterStatsLevel(String level) {
+        this.level = level;
+    }
+
+    public String getLevel() {
+        return level;
+    }
+
+    public static ClusterStatsLevel of(String level) {
+        for (ClusterStatsLevel value : values()) {
+            if (value.getLevel().equalsIgnoreCase(level)) {
+                return value;
+            }
+        }
+        throw new IllegalArgumentException("level parameter must be one of [cluster] or [indices] or [shards] but was [" + level + "]");
+    }
+
+    public static ClusterStatsLevel of(ToXContent.Params params, ClusterStatsLevel defaultLevel) {
+        return ClusterStatsLevel.of(params.param("level", defaultLevel.getLevel()));
+    }
+}

+ 40 - 0
server/src/main/java/org/elasticsearch/action/NodeStatsLevel.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action;
+
+import org.elasticsearch.xcontent.ToXContent;
+
+public enum NodeStatsLevel {
+    NODE("node"),
+    INDICES("indices"),
+    SHARDS("shards");
+
+    private final String level;
+
+    NodeStatsLevel(String level) {
+        this.level = level;
+    }
+
+    public String getLevel() {
+        return level;
+    }
+
+    public static NodeStatsLevel of(String level) {
+        for (NodeStatsLevel value : values()) {
+            if (value.getLevel().equalsIgnoreCase(level)) {
+                return value;
+            }
+        }
+        throw new IllegalArgumentException("level parameter must be one of [node] or [indices] or [shards] but was [" + level + "]");
+    }
+
+    public static NodeStatsLevel of(ToXContent.Params params, NodeStatsLevel defaultLevel) {
+        return NodeStatsLevel.of(params.param("level", defaultLevel.getLevel()));
+    }
+}

+ 0 - 28
server/src/main/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthRequest.java

@@ -20,7 +20,6 @@ import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.core.TimeValue;
 
 
 import java.io.IOException;
 import java.io.IOException;
-import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 
 
 public class ClusterHealthRequest extends MasterNodeReadRequest<ClusterHealthRequest> implements IndicesRequest.Replaceable {
 public class ClusterHealthRequest extends MasterNodeReadRequest<ClusterHealthRequest> implements IndicesRequest.Replaceable {
@@ -34,11 +33,6 @@ public class ClusterHealthRequest extends MasterNodeReadRequest<ClusterHealthReq
     private ActiveShardCount waitForActiveShards = ActiveShardCount.NONE;
     private ActiveShardCount waitForActiveShards = ActiveShardCount.NONE;
     private String waitForNodes = "";
     private String waitForNodes = "";
     private Priority waitForEvents = null;
     private Priority waitForEvents = null;
-    /**
-     * Only used by the high-level REST Client. Controls the details level of the health information returned.
-     * The default value is 'cluster'.
-     */
-    private Level level = Level.CLUSTER;
 
 
     public ClusterHealthRequest() {}
     public ClusterHealthRequest() {}
 
 
@@ -232,30 +226,8 @@ public class ClusterHealthRequest extends MasterNodeReadRequest<ClusterHealthReq
         return this.waitForEvents;
         return this.waitForEvents;
     }
     }
 
 
-    /**
-     * Set the level of detail for the health information to be returned.
-     * Only used by the high-level REST Client.
-     */
-    public void level(Level level) {
-        this.level = Objects.requireNonNull(level, "level must not be null");
-    }
-
-    /**
-     * Get the level of detail for the health information to be returned.
-     * Only used by the high-level REST Client.
-     */
-    public Level level() {
-        return level;
-    }
-
     @Override
     @Override
     public ActionRequestValidationException validate() {
     public ActionRequestValidationException validate() {
         return null;
         return null;
     }
     }
-
-    public enum Level {
-        CLUSTER,
-        INDICES,
-        SHARDS
-    }
 }
 }

+ 3 - 2
server/src/main/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponse.java

@@ -9,6 +9,7 @@
 package org.elasticsearch.action.admin.cluster.health;
 package org.elasticsearch.action.admin.cluster.health;
 
 
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.cluster.health.ClusterIndexHealth;
 import org.elasticsearch.cluster.health.ClusterIndexHealth;
@@ -356,8 +357,8 @@ public class ClusterHealthResponse extends ActionResponse implements StatusToXCo
         builder.humanReadableField(TASK_MAX_WAIT_TIME_IN_QUEUE_IN_MILLIS, TASK_MAX_WAIT_TIME_IN_QUEUE, getTaskMaxWaitingTime());
         builder.humanReadableField(TASK_MAX_WAIT_TIME_IN_QUEUE_IN_MILLIS, TASK_MAX_WAIT_TIME_IN_QUEUE, getTaskMaxWaitingTime());
         builder.percentageField(ACTIVE_SHARDS_PERCENT_AS_NUMBER, ACTIVE_SHARDS_PERCENT, getActiveShardsPercent());
         builder.percentageField(ACTIVE_SHARDS_PERCENT_AS_NUMBER, ACTIVE_SHARDS_PERCENT, getActiveShardsPercent());
 
 
-        String level = params.param("level", "cluster");
-        boolean outputIndices = "indices".equals(level) || "shards".equals(level);
+        ClusterStatsLevel level = ClusterStatsLevel.of(params, ClusterStatsLevel.CLUSTER);
+        boolean outputIndices = level == ClusterStatsLevel.INDICES || level == ClusterStatsLevel.SHARDS;
 
 
         if (outputIndices) {
         if (outputIndices) {
             builder.startObject(INDICES);
             builder.startObject(INDICES);

+ 1 - 1
server/src/main/java/org/elasticsearch/action/admin/cluster/node/stats/NodesStatsRequest.java

@@ -178,7 +178,7 @@ public class NodesStatsRequest extends BaseNodesRequest<NodesStatsRequest> {
         INGEST("ingest"),
         INGEST("ingest"),
         ADAPTIVE_SELECTION("adaptive_selection"),
         ADAPTIVE_SELECTION("adaptive_selection"),
         SCRIPT_CACHE("script_cache"),
         SCRIPT_CACHE("script_cache"),
-        INDEXING_PRESSURE("indexing_pressure"),;
+        INDEXING_PRESSURE("indexing_pressure");
 
 
         private String metricName;
         private String metricName;
 
 

+ 4 - 9
server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java

@@ -9,6 +9,7 @@
 package org.elasticsearch.action.admin.indices.stats;
 package org.elasticsearch.action.admin.indices.stats;
 
 
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.TransportVersion;
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.action.admin.indices.stats.IndexStats.IndexStatsBuilder;
 import org.elasticsearch.action.admin.indices.stats.IndexStats.IndexStatsBuilder;
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
 import org.elasticsearch.action.support.broadcast.ChunkedBroadcastResponse;
 import org.elasticsearch.action.support.broadcast.ChunkedBroadcastResponse;
@@ -178,14 +179,8 @@ public class IndicesStatsResponse extends ChunkedBroadcastResponse {
 
 
     @Override
     @Override
     protected Iterator<ToXContent> customXContentChunks(ToXContent.Params params) {
     protected Iterator<ToXContent> customXContentChunks(ToXContent.Params params) {
-        final String level = params.param("level", "indices");
-        final boolean isLevelValid = "cluster".equalsIgnoreCase(level)
-            || "indices".equalsIgnoreCase(level)
-            || "shards".equalsIgnoreCase(level);
-        if (isLevelValid == false) {
-            throw new IllegalArgumentException("level parameter must be one of [cluster] or [indices] or [shards] but was [" + level + "]");
-        }
-        if ("indices".equalsIgnoreCase(level) || "shards".equalsIgnoreCase(level)) {
+        final ClusterStatsLevel level = ClusterStatsLevel.of(params, ClusterStatsLevel.INDICES);
+        if (level == ClusterStatsLevel.INDICES || level == ClusterStatsLevel.SHARDS) {
             return Iterators.concat(Iterators.single(((builder, p) -> {
             return Iterators.concat(Iterators.single(((builder, p) -> {
                 commonStats(builder, p);
                 commonStats(builder, p);
                 return builder.startObject(Fields.INDICES);
                 return builder.startObject(Fields.INDICES);
@@ -206,7 +201,7 @@ public class IndicesStatsResponse extends ChunkedBroadcastResponse {
                 indexStats.getTotal().toXContent(builder, p);
                 indexStats.getTotal().toXContent(builder, p);
                 builder.endObject();
                 builder.endObject();
 
 
-                if ("shards".equalsIgnoreCase(level)) {
+                if (level == ClusterStatsLevel.SHARDS) {
                     builder.startObject(Fields.SHARDS);
                     builder.startObject(Fields.SHARDS);
                     for (IndexShardStats indexShardStats : indexStats) {
                     for (IndexShardStats indexShardStats : indexStats) {
                         builder.startArray(Integer.toString(indexShardStats.getShardId().id()));
                         builder.startArray(Integer.toString(indexShardStats.getShardId().id()));

+ 3 - 1
server/src/main/java/org/elasticsearch/cluster/health/ClusterIndexHealth.java

@@ -8,6 +8,7 @@
 
 
 package org.elasticsearch.cluster.health;
 package org.elasticsearch.cluster.health;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.routing.IndexRoutingTable;
 import org.elasticsearch.cluster.routing.IndexRoutingTable;
 import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
 import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
@@ -267,7 +268,8 @@ public final class ClusterIndexHealth implements Writeable, ToXContentFragment {
         builder.field(INITIALIZING_SHARDS, getInitializingShards());
         builder.field(INITIALIZING_SHARDS, getInitializingShards());
         builder.field(UNASSIGNED_SHARDS, getUnassignedShards());
         builder.field(UNASSIGNED_SHARDS, getUnassignedShards());
 
 
-        if ("shards".equals(params.param("level", "indices"))) {
+        ClusterStatsLevel level = ClusterStatsLevel.of(params, ClusterStatsLevel.INDICES);
+        if (level == ClusterStatsLevel.SHARDS) {
             builder.startObject(SHARDS);
             builder.startObject(SHARDS);
             for (ClusterShardHealth shardHealth : shards.values()) {
             for (ClusterShardHealth shardHealth : shards.values()) {
                 shardHealth.toXContent(builder, params);
                 shardHealth.toXContent(builder, params);

+ 6 - 10
server/src/main/java/org/elasticsearch/indices/NodeIndicesStats.java

@@ -9,6 +9,7 @@
 package org.elasticsearch.indices;
 package org.elasticsearch.indices;
 
 
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.TransportVersion;
+import org.elasticsearch.action.NodeStatsLevel;
 import org.elasticsearch.action.admin.indices.stats.CommonStats;
 import org.elasticsearch.action.admin.indices.stats.CommonStats;
 import org.elasticsearch.action.admin.indices.stats.IndexShardStats;
 import org.elasticsearch.action.admin.indices.stats.IndexShardStats;
 import org.elasticsearch.action.admin.indices.stats.ShardStats;
 import org.elasticsearch.action.admin.indices.stats.ShardStats;
@@ -216,19 +217,13 @@ public class NodeIndicesStats implements Writeable, ToXContentFragment {
 
 
     @Override
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        final String level = params.param("level", "node");
-        final boolean isLevelValid = "indices".equalsIgnoreCase(level)
-            || "node".equalsIgnoreCase(level)
-            || "shards".equalsIgnoreCase(level);
-        if (isLevelValid == false) {
-            throw new IllegalArgumentException("level parameter must be one of [indices] or [node] or [shards] but was [" + level + "]");
-        }
+        final NodeStatsLevel level = NodeStatsLevel.of(params, NodeStatsLevel.NODE);
 
 
         // "node" level
         // "node" level
         builder.startObject(Fields.INDICES);
         builder.startObject(Fields.INDICES);
         stats.toXContent(builder, params);
         stats.toXContent(builder, params);
 
 
-        if ("indices".equals(level)) {
+        if (level == NodeStatsLevel.INDICES) {
             Map<Index, CommonStats> indexStats = createCommonStatsByIndex();
             Map<Index, CommonStats> indexStats = createCommonStatsByIndex();
             builder.startObject(Fields.INDICES);
             builder.startObject(Fields.INDICES);
             for (Map.Entry<Index, CommonStats> entry : indexStats.entrySet()) {
             for (Map.Entry<Index, CommonStats> entry : indexStats.entrySet()) {
@@ -237,8 +232,8 @@ public class NodeIndicesStats implements Writeable, ToXContentFragment {
                 builder.endObject();
                 builder.endObject();
             }
             }
             builder.endObject();
             builder.endObject();
-        } else if ("shards".equals(level)) {
-            builder.startObject("shards");
+        } else if (level == NodeStatsLevel.SHARDS) {
+            builder.startObject(Fields.SHARDS);
             for (Map.Entry<Index, List<IndexShardStats>> entry : statsByShard.entrySet()) {
             for (Map.Entry<Index, List<IndexShardStats>> entry : statsByShard.entrySet()) {
                 builder.startArray(entry.getKey().getName());
                 builder.startArray(entry.getKey().getName());
                 for (IndexShardStats indexShardStats : entry.getValue()) {
                 for (IndexShardStats indexShardStats : entry.getValue()) {
@@ -285,5 +280,6 @@ public class NodeIndicesStats implements Writeable, ToXContentFragment {
 
 
     static final class Fields {
     static final class Fields {
         static final String INDICES = "indices";
         static final String INDICES = "indices";
+        static final String SHARDS = "shards";
     }
     }
 }
 }

+ 3 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterHealthAction.java

@@ -8,6 +8,7 @@
 
 
 package org.elasticsearch.rest.action.admin.cluster;
 package org.elasticsearch.rest.action.admin.cluster;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
 import org.elasticsearch.action.support.ActiveShardCount;
 import org.elasticsearch.action.support.ActiveShardCount;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.action.support.IndicesOptions;
@@ -81,6 +82,8 @@ public class RestClusterHealthAction extends BaseRestHandler {
         if (request.param("wait_for_events") != null) {
         if (request.param("wait_for_events") != null) {
             clusterHealthRequest.waitForEvents(Priority.valueOf(request.param("wait_for_events").toUpperCase(Locale.ROOT)));
             clusterHealthRequest.waitForEvents(Priority.valueOf(request.param("wait_for_events").toUpperCase(Locale.ROOT)));
         }
         }
+        // level parameter validation
+        ClusterStatsLevel.of(request, ClusterStatsLevel.CLUSTER);
         return clusterHealthRequest;
         return clusterHealthRequest;
     }
     }
 
 

+ 3 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesStatsAction.java

@@ -8,6 +8,7 @@
 
 
 package org.elasticsearch.rest.action.admin.cluster;
 package org.elasticsearch.rest.action.admin.cluster;
 
 
+import org.elasticsearch.action.NodeStatsLevel;
 import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest;
 import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags.Flag;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags.Flag;
@@ -86,6 +87,8 @@ public class RestNodesStatsAction extends BaseRestHandler {
 
 
         NodesStatsRequest nodesStatsRequest = new NodesStatsRequest(nodesIds);
         NodesStatsRequest nodesStatsRequest = new NodesStatsRequest(nodesIds);
         nodesStatsRequest.timeout(request.param("timeout"));
         nodesStatsRequest.timeout(request.param("timeout"));
+        // level parameter validation
+        NodeStatsLevel.of(request, NodeStatsLevel.NODE);
 
 
         if (metrics.size() == 1 && metrics.contains("_all")) {
         if (metrics.size() == 1 && metrics.contains("_all")) {
             if (request.hasParam("index_metric")) {
             if (request.hasParam("index_metric")) {

+ 3 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsAction.java

@@ -8,6 +8,7 @@
 
 
 package org.elasticsearch.rest.action.admin.indices;
 package org.elasticsearch.rest.action.admin.indices;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags.Flag;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags.Flag;
 import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest;
 import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest;
@@ -85,6 +86,8 @@ public class RestIndicesStatsAction extends BaseRestHandler {
             : "IndicesStats default indices options changed";
             : "IndicesStats default indices options changed";
         indicesStatsRequest.indicesOptions(IndicesOptions.fromRequest(request, defaultIndicesOption));
         indicesStatsRequest.indicesOptions(IndicesOptions.fromRequest(request, defaultIndicesOption));
         indicesStatsRequest.indices(Strings.splitStringByCommaToArray(request.param("index")));
         indicesStatsRequest.indices(Strings.splitStringByCommaToArray(request.param("index")));
+        // level parameter validation
+        ClusterStatsLevel.of(request, ClusterStatsLevel.INDICES);
 
 
         Set<String> metrics = Strings.tokenizeByCommaToSet(request.param("metric", "_all"));
         Set<String> metrics = Strings.tokenizeByCommaToSet(request.param("metric", "_all"));
         // short cut, if no metrics have been specified in URI
         // short cut, if no metrics have been specified in URI

+ 3 - 2
server/src/test/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponsesTests.java

@@ -8,6 +8,7 @@
 
 
 package org.elasticsearch.action.admin.cluster.health;
 package org.elasticsearch.action.admin.cluster.health;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
@@ -40,7 +41,7 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
 
 
 public class ClusterHealthResponsesTests extends AbstractXContentSerializingTestCase<ClusterHealthResponse> {
 public class ClusterHealthResponsesTests extends AbstractXContentSerializingTestCase<ClusterHealthResponse> {
-    private final ClusterHealthRequest.Level level = randomFrom(ClusterHealthRequest.Level.values());
+    private final ClusterStatsLevel level = randomFrom(ClusterStatsLevel.values());
 
 
     public void testIsTimeout() {
     public void testIsTimeout() {
         ClusterHealthResponse res = new ClusterHealthResponse();
         ClusterHealthResponse res = new ClusterHealthResponse();
@@ -109,7 +110,7 @@ public class ClusterHealthResponsesTests extends AbstractXContentSerializingTest
     protected ClusterHealthResponse createTestInstance() {
     protected ClusterHealthResponse createTestInstance() {
         int indicesSize = randomInt(20);
         int indicesSize = randomInt(20);
         Map<String, ClusterIndexHealth> indices = Maps.newMapWithExpectedSize(indicesSize);
         Map<String, ClusterIndexHealth> indices = Maps.newMapWithExpectedSize(indicesSize);
-        if (ClusterHealthRequest.Level.INDICES.equals(level) || ClusterHealthRequest.Level.SHARDS.equals(level)) {
+        if (level == ClusterStatsLevel.INDICES || level == ClusterStatsLevel.SHARDS) {
             for (int i = 0; i < indicesSize; i++) {
             for (int i = 0; i < indicesSize; i++) {
                 String indexName = randomAlphaOfLengthBetween(1, 5) + i;
                 String indexName = randomAlphaOfLengthBetween(1, 5) + i;
                 indices.put(indexName, ClusterIndexHealthTests.randomIndexHealth(indexName, level));
                 indices.put(indexName, ClusterIndexHealthTests.randomIndexHealth(indexName, level));

+ 4 - 4
server/src/test/java/org/elasticsearch/cluster/health/ClusterIndexHealthTests.java

@@ -8,7 +8,7 @@
 package org.elasticsearch.cluster.health;
 package org.elasticsearch.cluster.health;
 
 
 import org.elasticsearch.Version;
 import org.elasticsearch.Version;
-import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.routing.IndexRoutingTable;
 import org.elasticsearch.cluster.routing.IndexRoutingTable;
 import org.elasticsearch.cluster.routing.RoutingTableGenerator;
 import org.elasticsearch.cluster.routing.RoutingTableGenerator;
@@ -29,7 +29,7 @@ import java.util.regex.Pattern;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.equalTo;
 
 
 public class ClusterIndexHealthTests extends AbstractXContentSerializingTestCase<ClusterIndexHealth> {
 public class ClusterIndexHealthTests extends AbstractXContentSerializingTestCase<ClusterIndexHealth> {
-    private final ClusterHealthRequest.Level level = randomFrom(ClusterHealthRequest.Level.SHARDS, ClusterHealthRequest.Level.INDICES);
+    private final ClusterStatsLevel level = randomFrom(ClusterStatsLevel.SHARDS, ClusterStatsLevel.INDICES);
 
 
     public void testClusterIndexHealth() {
     public void testClusterIndexHealth() {
         RoutingTableGenerator routingTableGenerator = new RoutingTableGenerator();
         RoutingTableGenerator routingTableGenerator = new RoutingTableGenerator();
@@ -73,9 +73,9 @@ public class ClusterIndexHealthTests extends AbstractXContentSerializingTestCase
         return randomIndexHealth(randomAlphaOfLengthBetween(1, 10), level);
         return randomIndexHealth(randomAlphaOfLengthBetween(1, 10), level);
     }
     }
 
 
-    public static ClusterIndexHealth randomIndexHealth(String indexName, ClusterHealthRequest.Level level) {
+    public static ClusterIndexHealth randomIndexHealth(String indexName, ClusterStatsLevel level) {
         Map<Integer, ClusterShardHealth> shards = new HashMap<>();
         Map<Integer, ClusterShardHealth> shards = new HashMap<>();
-        if (level == ClusterHealthRequest.Level.SHARDS) {
+        if (level == ClusterStatsLevel.SHARDS) {
             for (int i = 0; i < randomInt(5); i++) {
             for (int i = 0; i < randomInt(5); i++) {
                 shards.put(i, ClusterShardHealthTests.randomShardHealth(i));
                 shards.put(i, ClusterShardHealthTests.randomShardHealth(i));
             }
             }

+ 1 - 1
server/src/test/java/org/elasticsearch/indices/NodeIndicesStatsTests.java

@@ -25,7 +25,7 @@ public class NodeIndicesStatsTests extends ESTestCase {
         final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> stats.toXContent(null, params));
         final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> stats.toXContent(null, params));
         assertThat(
         assertThat(
             e,
             e,
-            hasToString(containsString("level parameter must be one of [indices] or [node] or [shards] but was [" + level + "]"))
+            hasToString(containsString("level parameter must be one of [node] or [indices] or [shards] but was [" + level + "]"))
         );
         );
     }
     }
 
 

+ 44 - 0
server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestClusterHealthActionTests.java

@@ -8,8 +8,11 @@
 
 
 package org.elasticsearch.rest.action.admin.cluster;
 package org.elasticsearch.rest.action.admin.cluster;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
+import org.elasticsearch.action.NodeStatsLevel;
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
 import org.elasticsearch.action.support.ActiveShardCount;
 import org.elasticsearch.action.support.ActiveShardCount;
+import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.common.Priority;
 import org.elasticsearch.common.Priority;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.core.TimeValue;
@@ -17,10 +20,14 @@ import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.rest.FakeRestRequest;
 import org.elasticsearch.test.rest.FakeRestRequest;
 
 
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.object.HasToString.hasToString;
+import static org.mockito.Mockito.mock;
 
 
 public class RestClusterHealthActionTests extends ESTestCase {
 public class RestClusterHealthActionTests extends ESTestCase {
 
 
@@ -68,6 +75,43 @@ public class RestClusterHealthActionTests extends ESTestCase {
 
 
     }
     }
 
 
+    public void testLevelValidation() throws IOException {
+        RestClusterHealthAction action = new RestClusterHealthAction();
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("level", ClusterStatsLevel.CLUSTER.getLevel());
+
+        // cluster is valid
+        RestRequest request = buildRestRequest(params);
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // indices is valid
+        params.put("level", ClusterStatsLevel.INDICES.getLevel());
+        request = buildRestRequest(params);
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // shards is valid
+        params.put("level", ClusterStatsLevel.SHARDS.getLevel());
+        request = buildRestRequest(params);
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        params.put("level", NodeStatsLevel.NODE.getLevel());
+        final RestRequest invalidLevelRequest1 = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats")
+            .withParams(params)
+            .build();
+
+        IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(invalidLevelRequest1, mock(NodeClient.class))
+        );
+        assertThat(e, hasToString(containsString("level parameter must be one of [cluster] or [indices] or [shards] but was [node]")));
+
+        params.put("level", "invalid");
+        final RestRequest invalidLevelRequest = buildRestRequest(params);
+
+        e = expectThrows(IllegalArgumentException.class, () -> action.prepareRequest(invalidLevelRequest, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("level parameter must be one of [cluster] or [indices] or [shards] but was [invalid]")));
+    }
+
     private FakeRestRequest buildRestRequest(Map<String, String> params) {
     private FakeRestRequest buildRestRequest(Map<String, String> params) {
         return new FakeRestRequest.Builder(xContentRegistry()).withMethod(RestRequest.Method.GET)
         return new FakeRestRequest.Builder(xContentRegistry()).withMethod(RestRequest.Method.GET)
             .withPath("/_cluster/health")
             .withPath("/_cluster/health")

+ 40 - 0
server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestNodesStatsActionTests.java

@@ -8,6 +8,8 @@
 
 
 package org.elasticsearch.rest.action.admin.cluster;
 package org.elasticsearch.rest.action.admin.cluster;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
+import org.elasticsearch.action.NodeStatsLevel;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.ESTestCase;
@@ -142,4 +144,42 @@ public class RestNodesStatsActionTests extends ESTestCase {
         );
         );
     }
     }
 
 
+    public void testLevelValidation() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("metric", "_all");
+        params.put("level", NodeStatsLevel.NODE.getLevel());
+
+        // node is valid
+        RestRequest request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_nodes/stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // indices is valid
+        params.put("level", NodeStatsLevel.INDICES.getLevel());
+        request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_nodes/stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // shards is valid
+        params.put("level", NodeStatsLevel.SHARDS.getLevel());
+        request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_nodes/stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        params.put("level", ClusterStatsLevel.CLUSTER.getLevel());
+        final RestRequest invalidLevelRequest1 = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats")
+            .withParams(params)
+            .build();
+
+        IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(invalidLevelRequest1, mock(NodeClient.class))
+        );
+        assertThat(e, hasToString(containsString("level parameter must be one of [node] or [indices] or [shards] but was [cluster]")));
+
+        params.put("level", "invalid");
+        final RestRequest invalidLevelRequest = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_nodes/stats")
+            .withParams(params)
+            .build();
+
+        e = expectThrows(IllegalArgumentException.class, () -> action.prepareRequest(invalidLevelRequest, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("level parameter must be one of [node] or [indices] or [shards] but was [invalid]")));
+    }
 }
 }

+ 39 - 0
server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsActionTests.java

@@ -8,6 +8,8 @@
 
 
 package org.elasticsearch.rest.action.admin.indices;
 package org.elasticsearch.rest.action.admin.indices;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
+import org.elasticsearch.action.NodeStatsLevel;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.ESTestCase;
@@ -70,4 +72,41 @@ public class RestIndicesStatsActionTests extends ESTestCase {
         assertThat(e, hasToString(containsString("request [/_stats] contains _all and individual metrics [_all," + metric + "]")));
         assertThat(e, hasToString(containsString("request [/_stats] contains _all and individual metrics [_all," + metric + "]")));
     }
     }
 
 
+    public void testLevelValidation() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("level", ClusterStatsLevel.CLUSTER.getLevel());
+
+        // cluster is valid
+        RestRequest request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // indices is valid
+        params.put("level", ClusterStatsLevel.INDICES.getLevel());
+        request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // shards is valid
+        params.put("level", ClusterStatsLevel.SHARDS.getLevel());
+        request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        params.put("level", NodeStatsLevel.NODE.getLevel());
+        final RestRequest invalidLevelRequest1 = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats")
+            .withParams(params)
+            .build();
+
+        IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(invalidLevelRequest1, mock(NodeClient.class))
+        );
+        assertThat(e, hasToString(containsString("level parameter must be one of [cluster] or [indices] or [shards] but was [node]")));
+
+        params.put("level", "invalid");
+        final RestRequest invalidLevelRequest2 = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats")
+            .withParams(params)
+            .build();
+
+        e = expectThrows(IllegalArgumentException.class, () -> action.prepareRequest(invalidLevelRequest2, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("level parameter must be one of [cluster] or [indices] or [shards] but was [invalid]")));
+    }
 }
 }

+ 4 - 9
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponse.java

@@ -6,6 +6,7 @@
  */
  */
 package org.elasticsearch.xpack.searchablesnapshots.action;
 package org.elasticsearch.xpack.searchablesnapshots.action;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
 import org.elasticsearch.action.support.broadcast.BroadcastResponse;
 import org.elasticsearch.action.support.broadcast.BroadcastResponse;
 import org.elasticsearch.cluster.routing.ShardRouting;
 import org.elasticsearch.cluster.routing.ShardRouting;
@@ -77,13 +78,7 @@ public class SearchableSnapshotsStatsResponse extends BroadcastResponse {
 
 
     @Override
     @Override
     protected void addCustomXContentFields(XContentBuilder builder, Params params) throws IOException {
     protected void addCustomXContentFields(XContentBuilder builder, Params params) throws IOException {
-        final String level = params.param("level", "indices");
-        final boolean isLevelValid = "cluster".equalsIgnoreCase(level)
-            || "indices".equalsIgnoreCase(level)
-            || "shards".equalsIgnoreCase(level);
-        if (isLevelValid == false) {
-            throw new IllegalArgumentException("level parameter must be one of [cluster] or [indices] or [shards] but was [" + level + "]");
-        }
+        final ClusterStatsLevel level = ClusterStatsLevel.of(params, ClusterStatsLevel.INDICES);
 
 
         builder.startArray("total");
         builder.startArray("total");
         for (CacheIndexInputStats cis : getTotal()) {
         for (CacheIndexInputStats cis : getTotal()) {
@@ -91,7 +86,7 @@ public class SearchableSnapshotsStatsResponse extends BroadcastResponse {
         }
         }
         builder.endArray();
         builder.endArray();
 
 
-        if ("indices".equalsIgnoreCase(level) || "shards".equalsIgnoreCase(level)) {
+        if (level == ClusterStatsLevel.INDICES || level == ClusterStatsLevel.SHARDS) {
             builder.startObject("indices");
             builder.startObject("indices");
             final List<Index> indices = getStats().stream()
             final List<Index> indices = getStats().stream()
                 .filter(shardStats -> shardStats.getStats().isEmpty() == false)
                 .filter(shardStats -> shardStats.getStats().isEmpty() == false)
@@ -116,7 +111,7 @@ public class SearchableSnapshotsStatsResponse extends BroadcastResponse {
                     }
                     }
                     builder.endArray();
                     builder.endArray();
 
 
-                    if ("shards".equalsIgnoreCase(level)) {
+                    if (level == ClusterStatsLevel.SHARDS) {
                         builder.startObject("shards");
                         builder.startObject("shards");
                         {
                         {
                             List<SearchableSnapshotShardStats> listOfStats = getStats().stream()
                             List<SearchableSnapshotShardStats> listOfStats = getStats().stream()

+ 6 - 5
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestSearchableSnapshotsStatsAction.java

@@ -6,6 +6,7 @@
  */
  */
 package org.elasticsearch.xpack.searchablesnapshots.rest;
 package org.elasticsearch.xpack.searchablesnapshots.rest;
 
 
+import org.elasticsearch.action.ClusterStatsLevel;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.BaseRestHandler;
@@ -35,11 +36,11 @@ public class RestSearchableSnapshotsStatsAction extends BaseRestHandler {
     @Override
     @Override
     public RestChannelConsumer prepareRequest(final RestRequest restRequest, final NodeClient client) {
     public RestChannelConsumer prepareRequest(final RestRequest restRequest, final NodeClient client) {
         String[] indices = Strings.splitStringByCommaToArray(restRequest.param("index"));
         String[] indices = Strings.splitStringByCommaToArray(restRequest.param("index"));
-        return channel -> client.execute(
-            SearchableSnapshotsStatsAction.INSTANCE,
-            new SearchableSnapshotsStatsRequest(indices),
-            new RestToXContentListener<>(channel)
-        );
+        SearchableSnapshotsStatsRequest statsRequest = new SearchableSnapshotsStatsRequest(indices);
+        // level parameter validation
+        ClusterStatsLevel.of(restRequest, ClusterStatsLevel.INDICES);
+
+        return channel -> client.execute(SearchableSnapshotsStatsAction.INSTANCE, statsRequest, new RestToXContentListener<>(channel));
     }
     }
 
 
     private static final Set<String> RESPONSE_PARAMS = Collections.singleton("level");
     private static final Set<String> RESPONSE_PARAMS = Collections.singleton("level");

+ 51 - 0
x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponseTests.java

@@ -8,21 +8,31 @@
 package org.elasticsearch.xpack.searchablesnapshots.action;
 package org.elasticsearch.xpack.searchablesnapshots.action;
 
 
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.TransportVersion;
+import org.elasticsearch.action.ClusterStatsLevel;
+import org.elasticsearch.action.NodeStatsLevel;
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
+import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.cluster.routing.ShardRouting;
 import org.elasticsearch.cluster.routing.ShardRouting;
 import org.elasticsearch.cluster.routing.ShardRoutingState;
 import org.elasticsearch.cluster.routing.ShardRoutingState;
 import org.elasticsearch.common.unit.ByteSizeValue;
 import org.elasticsearch.common.unit.ByteSizeValue;
 import org.elasticsearch.repositories.IndexId;
 import org.elasticsearch.repositories.IndexId;
+import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.snapshots.SnapshotId;
 import org.elasticsearch.snapshots.SnapshotId;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.rest.FakeRestRequest;
 import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats;
 import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats;
+import org.elasticsearch.xpack.searchablesnapshots.rest.RestSearchableSnapshotsStatsAction;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 
 
 import static org.elasticsearch.cluster.routing.TestShardRouting.newShardRouting;
 import static org.elasticsearch.cluster.routing.TestShardRouting.newShardRouting;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.object.HasToString.hasToString;
+import static org.mockito.Mockito.mock;
 
 
 public class SearchableSnapshotsStatsResponseTests extends ESTestCase {
 public class SearchableSnapshotsStatsResponseTests extends ESTestCase {
 
 
@@ -39,6 +49,47 @@ public class SearchableSnapshotsStatsResponseTests extends ESTestCase {
         }
         }
     }
     }
 
 
+    public void testLevelValidation() {
+        RestSearchableSnapshotsStatsAction action = new RestSearchableSnapshotsStatsAction();
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("level", ClusterStatsLevel.CLUSTER.getLevel());
+
+        // cluster is valid
+        RestRequest request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_searchable_snapshots/stats")
+            .withParams(params)
+            .build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // indices is valid
+        params.put("level", ClusterStatsLevel.INDICES.getLevel());
+        request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_searchable_snapshots/stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        // shards is valid
+        params.put("level", ClusterStatsLevel.SHARDS.getLevel());
+        request = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_searchable_snapshots/stats").withParams(params).build();
+        action.prepareRequest(request, mock(NodeClient.class));
+
+        params.put("level", NodeStatsLevel.NODE.getLevel());
+        final RestRequest invalidLevelRequest1 = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats")
+            .withParams(params)
+            .build();
+
+        IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(invalidLevelRequest1, mock(NodeClient.class))
+        );
+        assertThat(e, hasToString(containsString("level parameter must be one of [cluster] or [indices] or [shards] but was [node]")));
+
+        params.put("level", "invalid");
+        final RestRequest invalidLevelRequest = new FakeRestRequest.Builder(xContentRegistry()).withPath("/_stats")
+            .withParams(params)
+            .build();
+
+        e = expectThrows(IllegalArgumentException.class, () -> action.prepareRequest(invalidLevelRequest, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("level parameter must be one of [cluster] or [indices] or [shards] but was [invalid]")));
+    }
+
     private void assertEqualInstances(SearchableSnapshotsStatsResponse expected, SearchableSnapshotsStatsResponse actual) {
     private void assertEqualInstances(SearchableSnapshotsStatsResponse expected, SearchableSnapshotsStatsResponse actual) {
         assertThat(actual.getTotalShards(), equalTo(expected.getTotalShards()));
         assertThat(actual.getTotalShards(), equalTo(expected.getTotalShards()));
         assertThat(actual.getSuccessfulShards(), equalTo(expected.getSuccessfulShards()));
         assertThat(actual.getSuccessfulShards(), equalTo(expected.getSuccessfulShards()));