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

mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 364e145c80 [#12834] fix(server): reject null request bodies in 
remaining REST operations (#12866)
364e145c80 is described below

commit 364e145c804f6c753ed3b5ad0bd8478e170d99b3
Author: Shuai Liu <[email protected]>
AuthorDate: Thu Sep 3 18:03:54 2026 +0800

    [#12834] fix(server): reject null request bodies in remaining REST 
operations (#12866)
    
    ### What changes were proposed in this pull request?
    
    - Reject a null request body in the 20 operations listed in #12834,
    using operation-level guards in 9 resource classes.
    - Move the pre-existing `updateStatistics` guard out of the `Utils.doAs`
    lambda to the front of the method and drop its private message constant,
    so all four operations in `StatisticOperations` report the same message.
    - Return the 400 for `testConnection` directly through
    `Utils.illegalArguments(...)` instead of
    `handleTestConnectionException`, which by design reports connection-test
    outcomes inside an HTTP 200 body.
    - Follow the existing pattern of `MetalakeOperations.createMetalake`.
    - Simplify the null-safe field extractions used for logging and are now
    unreachable, in `testConnection`, `PermissionOperations` and
    `StatisticOperations`.
    - Add regression coverage for every fixed operation, plus a
    malformed-JSON test and a normal-path `setMetalake` test.
    
    
    
    ### Why are the changes needed?
    
    An omitted body, an empty body, or a JSON literal `null` is a client
    error, but these endpoints answer it with HTTP 500.
    
    Fix: #12834
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. These endpoints now return HTTP 400 with error code `1001`, error
    type `IllegalArgumentException`, and a message stating that the request
    body cannot be null, instead of HTTP 500.
    
    One existing message also changes: a null body on `PUT
    /api/metalakes/{metalake}/objects/{type}/{fullName}/statistics`
    previously returned `Statistics update request body cannot be null` and
    now returns the canonical `Request body cannot be null`. Its status code
    and error code are unchanged.
    
    There are no API schema or configuration changes, valid requests behave
    exactly as before, and malformed-JSON handling is unchanged.
    
    ### How was this patch tested?
    
    Added 21 `testXxxWithNullRequest()` tests, one per fixed operation plus
    one for the deprecated tag route, using the shared
    `assertNullRequestBodyRejected` helper introduced by #12770.
    `TestStatisticOperations` and `TestMetadataObjectPolicyOperations` now
    extend `BaseOperationsTest` to use it, and the pre-existing
    `testUpdateTableStatisticsWithNullRequestBody` was converged onto it.
    Also added `testSetMetalakeWithMalformedJson`, which sends `{` and
    asserts the response still comes from the JSON exception mappers, and
    `testSetMetalake` for the normal enable/disable path, which had no
    coverage before.
    
    With the production changes reverted and only the tests applied, all 21
    null-body tests fail with:
    
    ```text
    expected: <400> but was: <500>
    ```
    
    With the fix applied:
    
    ```shell
    ./gradlew :server:test -PskipITs --no-daemon        # 396 tests, 0 
failures, 0 errors
    ./gradlew :server:spotlessCheck --no-daemon
    git diff --check
    ```
    
    
    ### Note for reviewers
    
    With `gravitino.authorization.enable=true` the two `associate*`
    endpoints never reach the new guard: `AssociateTagAuthorizationExecutor`
    / `AssociatePolicyAuthorizationExecutor` return `false` for a null
    request, so `GravitinoInterceptionService` answers 403 before the
    resource method runs. The guard is still correct for the default
    configuration, where that service is not registered at all. I left the
    executors alone as changing an authorization decision felt out of scope;
    a follow-up would have to `return true` there, so that a null body
    reaches the guard in the resource method and is rejected with the 400.
    Happy to file that issue if you would like it handled.
---
 .../gravitino/server/web/rest/BulkOperations.java  | 16 +++++
 .../server/web/rest/CatalogOperations.java         | 18 ++++-
 .../web/rest/MetadataObjectPolicyOperations.java   |  9 ++-
 .../web/rest/MetadataObjectTagOperations.java      | 18 +++++
 .../server/web/rest/MetalakeOperations.java        |  7 ++
 .../gravitino/server/web/rest/OwnerOperations.java |  8 +++
 .../server/web/rest/PermissionOperations.java      | 64 ++++++++++++++++--
 .../server/web/rest/PolicyOperations.java          |  9 ++-
 .../server/web/rest/StatisticOperations.java       | 50 ++++++++++----
 .../server/web/rest/TestBulkOperations.java        | 22 ++++++
 .../server/web/rest/TestCatalogOperations.java     | 28 ++++++++
 .../rest/TestMetadataObjectPolicyOperations.java   | 19 +++++-
 .../web/rest/TestMetadataObjectTagOperations.java  | 34 ++++++++++
 .../server/web/rest/TestMetalakeOperations.java    | 63 +++++++++++++++++
 .../server/web/rest/TestOwnerOperations.java       | 11 +++
 .../server/web/rest/TestPermissionOperations.java  | 77 +++++++++++++++++++++
 .../server/web/rest/TestPolicyOperations.java      | 13 ++++
 .../server/web/rest/TestStatisticOperations.java   | 79 ++++++++++++++++++++--
 .../server/web/rest/TestTagOperations.java         | 16 +++++
 19 files changed, 532 insertions(+), 29 deletions(-)

diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java
index 9c9eb8796f..58be4935a3 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java
@@ -158,6 +158,14 @@ public class BulkOperations {
       @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
           String metalake,
       BulkRemoveRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleUserException(
+          OperationType.REMOVE,
+          "",
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
       return Utils.doAs(
           httpRequest,
@@ -264,6 +272,14 @@ public class BulkOperations {
       @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
           String metalake,
       BulkRemoveRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleGroupException(
+          OperationType.REMOVE,
+          "",
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
       return Utils.doAs(
           httpRequest,
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
index ea5d99cdbd..9aab629818 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
@@ -192,7 +192,14 @@ public class CatalogOperations {
       @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
           String metalake,
       CatalogCreateRequest request) {
-    String catalogName = request == null ? "" : request.getName();
+    if (request == null) {
+      // Unlike a failed connection test, which 
handleTestConnectionException() reports inside an
+      // HTTP 200 response by design, a missing request body never reaches the 
connection test, so
+      // it is rejected with a regular HTTP 400.
+      return Utils.illegalArguments("Request body cannot be null");
+    }
+
+    String catalogName = request.getName();
     LOG.info("Received test connection request for catalog: {}.{}", metalake, 
catalogName);
     try {
       return Utils.doAs(
@@ -264,8 +271,15 @@ public class CatalogOperations {
       @PathParam("catalog") @AuthorizationMetadata(type = 
Entity.EntityType.CATALOG)
           String catalogName,
       CatalogSetRequest request) {
-    LOG.info("Received set request for catalog: {}.{}", metalake, catalogName);
+    if (request == null) {
+      return ExceptionHandlers.handleCatalogException(
+          OperationType.SET,
+          catalogName,
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
 
+    LOG.info("Received set request for catalog: {}.{}", metalake, catalogName);
     OperationType op = request.isInUse() ? OperationType.ENABLE : 
OperationType.DISABLE;
 
     try {
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
index 78a03570fb..19ea45ab96 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
@@ -251,12 +251,19 @@ public class MetadataObjectPolicyOperations {
       @PathParam("fullName") @AuthorizationFullName String fullName,
       @AuthorizationRequest(type = 
AuthorizationRequest.RequestType.ASSOCIATE_POLICY)
           PoliciesAssociateRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handlePolicyException(
+          OperationType.ASSOCIATE,
+          "",
+          fullName,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     LOG.info(
         "Received associate policies request for object type: {}, full name: 
{} under metalake: {}",
         type,
         fullName,
         metalake);
-
     try {
       return Utils.doAs(
           httpRequest,
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
index a77584b3cb..072f3876cc 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
@@ -289,6 +289,14 @@ public class MetadataObjectTagOperations {
 
   private Response associateTagsForObjectInternal(
       String metalake, String type, String fullName, TagsAssociateRequest 
request) {
+    if (request == null) {
+      return ExceptionHandlers.handleTagException(
+          OperationType.ASSOCIATE,
+          "",
+          fullName,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     LOG.info(
         "Received associate tags request for object type: {}, full name: {} 
under metalake: {}",
         type,
@@ -314,6 +322,16 @@ public class MetadataObjectTagOperations {
 
   private Response associateTagValuesForObjectInternal(
       String metalake, String type, String fullName, TagValuesAssociateRequest 
request) {
+    if (request == null) {
+      return withMediaType(
+          ExceptionHandlers.handleTagException(
+              OperationType.ASSOCIATE,
+              "",
+              fullName,
+              new IllegalArgumentException("Request body cannot be null")),
+          TAG_VALUES_MEDIA_TYPE);
+    }
+
     LOG.info(
         "Received associate tag values request for object type: {}, full name: 
{} under metalake: {}",
         type,
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
index 8d511a01dd..5e6dd53b9b 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
@@ -181,6 +181,13 @@ public class MetalakeOperations {
       @PathParam("name") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
           String metalakeName,
       MetalakeSetRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleMetalakeException(
+          OperationType.SET,
+          metalakeName,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     LOG.info("Received set request for metalake: {}", metalakeName);
     try {
       return Utils.doAs(
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/OwnerOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/OwnerOperations.java
index dbe91956b0..962dfde6a2 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/OwnerOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/OwnerOperations.java
@@ -119,6 +119,14 @@ public class OwnerOperations {
       @PathParam("metadataObjectType") String metadataObjectType,
       @PathParam("fullName") String fullName,
       OwnerSetRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleOwnerException(
+          OperationType.SET,
+          String.format("metadata object %s", fullName),
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
       MetadataObject object =
           MetadataObjects.parse(
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
index b7d0690f5a..bce5335d9b 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
@@ -88,7 +88,15 @@ public class PermissionOperations {
           String metalake,
       @PathParam("user") String user,
       RoleGrantRequest request) {
-    String roleNames = request == null ? "" : 
StringUtils.join(request.getRoleNames(), ",");
+    if (request == null) {
+      return ExceptionHandlers.handleUserPermissionOperationException(
+          OperationType.GRANT,
+          "",
+          user,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
+    String roleNames = StringUtils.join(request.getRoleNames(), ",");
     try {
       return Utils.doAs(
           httpRequest,
@@ -118,7 +126,15 @@ public class PermissionOperations {
           String metalake,
       @PathParam("group") String group,
       RoleGrantRequest request) {
-    String roleNames = request == null ? "" : 
StringUtils.join(request.getRoleNames(), ",");
+    if (request == null) {
+      return ExceptionHandlers.handleGroupPermissionOperationException(
+          OperationType.GRANT,
+          "",
+          group,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
+    String roleNames = StringUtils.join(request.getRoleNames(), ",");
     try {
       return Utils.doAs(
           httpRequest,
@@ -148,7 +164,15 @@ public class PermissionOperations {
           String metalake,
       @PathParam("user") String user,
       RoleRevokeRequest request) {
-    String roleNames = request == null ? "" : 
StringUtils.join(request.getRoleNames(), ",");
+    if (request == null) {
+      return ExceptionHandlers.handleUserPermissionOperationException(
+          OperationType.REVOKE,
+          "",
+          user,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
+    String roleNames = StringUtils.join(request.getRoleNames(), ",");
     try {
       return Utils.doAs(
           httpRequest,
@@ -178,7 +202,15 @@ public class PermissionOperations {
           String metalake,
       @PathParam("group") String group,
       RoleRevokeRequest request) {
-    String roleNames = request == null ? "" : 
StringUtils.join(request.getRoleNames(), ",");
+    if (request == null) {
+      return ExceptionHandlers.handleGroupPermissionOperationException(
+          OperationType.REVOKE,
+          "",
+          group,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
+    String roleNames = StringUtils.join(request.getRoleNames(), ",");
     try {
       return Utils.doAs(
           httpRequest,
@@ -212,6 +244,14 @@ public class PermissionOperations {
       @PathParam("type") String type,
       @PathParam("fullName") String fullName,
       PrivilegeGrantRequest privilegeGrantRequest) {
+    if (privilegeGrantRequest == null) {
+      return ExceptionHandlers.handleRolePermissionOperationException(
+          OperationType.GRANT,
+          fullName,
+          role,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
       MetadataObject object =
           MetadataObjects.parse(
@@ -260,6 +300,14 @@ public class PermissionOperations {
       @PathParam("type") String type,
       @PathParam("fullName") String fullName,
       PrivilegeRevokeRequest privilegeRevokeRequest) {
+    if (privilegeRevokeRequest == null) {
+      return ExceptionHandlers.handleRolePermissionOperationException(
+          OperationType.REVOKE,
+          fullName,
+          role,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
       MetadataObject object =
           MetadataObjects.parse(
@@ -304,6 +352,14 @@ public class PermissionOperations {
           String metalake,
       @PathParam("role") String role,
       PrivilegeOverrideRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleRolePermissionOperationException(
+          OperationType.UPDATE,
+          role,
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
 
       return Utils.doAs(
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
index ec336b74ae..1816356524 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
@@ -253,8 +253,15 @@ public class PolicyOperations {
           String metalake,
       @PathParam("policy") @AuthorizationMetadata(type = 
Entity.EntityType.POLICY) String name,
       PolicySetRequest request) {
-    LOG.info("Received set policy request for policy: {} under metalake: {}", 
name, metalake);
+    if (request == null) {
+      return ExceptionHandlers.handlePolicyException(
+          OperationType.SET,
+          name,
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
 
+    LOG.info("Received set policy request for policy: {} under metalake: {}", 
name, metalake);
     OperationType op = request.isEnable() ? OperationType.ENABLE : 
OperationType.DISABLE;
 
     try {
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/StatisticOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/StatisticOperations.java
index 5b99050d95..79887df7dd 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/StatisticOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/StatisticOperations.java
@@ -81,9 +81,6 @@ public class StatisticOperations {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(StatisticOperations.class);
 
-  private static final String NULL_STATS_UPDATE_REQUEST_BODY_ERROR =
-      "Statistics update request body cannot be null";
-
   @Context private HttpServletRequest httpRequest;
 
   private final StatisticDispatcher statisticDispatcher;
@@ -154,6 +151,14 @@ public class StatisticOperations {
       @PathParam("type") @AuthorizationObjectType String type,
       @PathParam("fullName") @AuthorizationFullName String fullName,
       StatisticsUpdateRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleStatisticException(
+          OperationType.UPDATE,
+          "",
+          fullName,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     String statisticNames = getStatisticNames(request);
     try {
       LOG.info(
@@ -164,9 +169,6 @@ public class StatisticOperations {
       return Utils.doAs(
           httpRequest,
           () -> {
-            if (request == null) {
-              throw new 
IllegalArgumentException(NULL_STATS_UPDATE_REQUEST_BODY_ERROR);
-            }
             request.validate();
             MetadataObject object =
                 MetadataObjects.parse(
@@ -215,10 +217,16 @@ public class StatisticOperations {
       @PathParam("type") @AuthorizationObjectType String type,
       @PathParam("fullName") @AuthorizationFullName String fullName,
       StatisticsDropRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleStatisticException(
+          OperationType.DROP,
+          "",
+          fullName,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     String statisticNames =
-        request == null || request.getNames() == null
-            ? ""
-            : StringUtils.join(request.getNames(), ",");
+        request.getNames() == null ? "" : StringUtils.join(request.getNames(), 
",");
     try {
       LOG.info(
           "Received drop statistics request for object full name: {} type: {} 
in the metalake {}",
@@ -358,8 +366,16 @@ public class StatisticOperations {
       @PathParam("type") @AuthorizationObjectType String type,
       @PathParam("fullName") @AuthorizationFullName String fullName,
       PartitionStatisticsUpdateRequest request) {
-    String partitions = getPartitionNames(request);
+    if (request == null) {
+      return ExceptionHandlers.handlePartitionStatsException(
+          OperationType.UPDATE,
+          "",
+          fullName,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     LOG.info("Updating partition statistics for table: {} in the metalake {}", 
fullName, metalake);
+    String partitions = getPartitionNames(request);
     try {
       return Utils.doAs(
           httpRequest,
@@ -432,6 +448,14 @@ public class StatisticOperations {
       @PathParam("type") @AuthorizationObjectType String type,
       @PathParam("fullName") @AuthorizationFullName String fullName,
       PartitionStatisticsDropRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handlePartitionStatsException(
+          OperationType.DROP,
+          "",
+          fullName,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     String partitions = getDropPartitionNames(request);
 
     try {
@@ -503,7 +527,7 @@ public class StatisticOperations {
   }
 
   private static String getStatisticNames(StatisticsUpdateRequest request) {
-    if (request == null || request.getUpdates() == null) {
+    if (request.getUpdates() == null) {
       return "";
     }
 
@@ -511,7 +535,7 @@ public class StatisticOperations {
   }
 
   private static String getPartitionNames(PartitionStatisticsUpdateRequest 
request) {
-    if (request == null || request.getUpdates() == null) {
+    if (request.getUpdates() == null) {
       return "";
     }
 
@@ -523,7 +547,7 @@ public class StatisticOperations {
   }
 
   private static String getDropPartitionNames(PartitionStatisticsDropRequest 
request) {
-    if (request == null || request.getDrops() == null) {
+    if (request.getDrops() == null) {
       return "";
     }
 
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
index d309b1567b..5c008c19c9 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
@@ -303,6 +303,28 @@ public class TestBulkOperations extends BaseOperationsTest 
{
     Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
bulkResponse.getErrors()[0].getCode());
   }
 
+  @Test
+  public void testRemoveUsersWithNullRequest() {
+    Response response =
+        target("/bulk/metalakes/metalake1/users/remove")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(response);
+  }
+
+  @Test
+  public void testRemoveGroupsWithNullRequest() {
+    Response response =
+        target("/bulk/metalakes/metalake1/groups/remove")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(new byte[0], MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(response);
+  }
+
   @Test
   public void testBulkRejectsEmptyAndExceededRequest() {
     Response emptyResponse =
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
index ebbc7a52a1..363a03c322 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
@@ -30,6 +30,8 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
@@ -316,6 +318,20 @@ public class TestCatalogOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse2.getType());
   }
 
+  @Test
+  public void testTestConnectionWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/catalogs/testConnection")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    // A missing request body never reaches the connection test, so it is 
reported as a regular
+    // HTTP 400 rather than through the HTTP 200 envelope used for 
connection-test failures.
+    assertNullRequestBodyRejected(resp);
+    verify(manager, never()).testConnection(any(), any(), any(), any(), any());
+  }
+
   @Test
   public void testConnection() {
     CatalogCreateRequest req =
@@ -693,6 +709,18 @@ public class TestCatalogOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse1.getType());
   }
 
+  @Test
+  public void testSetCatalogWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/catalogs/catalog1")
+            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .method("PATCH", Entity.entity("null", 
MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
   private static TestCatalog buildCatalogWithProperties(
       String metalake, String catalogName, Map<String, String> properties) {
     CatalogEntity entity =
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
index e95b06fb88..6ff31e2203 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
@@ -57,12 +57,11 @@ import org.apache.gravitino.policy.PolicyManager;
 import org.apache.gravitino.rest.RESTUtils;
 import org.glassfish.jersey.internal.inject.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
-import org.glassfish.jersey.test.JerseyTest;
 import org.glassfish.jersey.test.TestProperties;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-public class TestMetadataObjectPolicyOperations extends JerseyTest {
+public class TestMetadataObjectPolicyOperations extends BaseOperationsTest {
 
   private static class MockServletRequestFactory extends 
ServletRequestFactoryBase {
 
@@ -669,4 +668,20 @@ public class TestMetadataObjectPolicyOperations extends 
JerseyTest {
         .withAuditInfo(testAuditInfo1)
         .build();
   }
+
+  @Test
+  public void testAssociatePoliciesForObjectWithNullRequest() {
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+
+    Response response =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("policies")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(response);
+  }
 }
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
index 1a291c305e..82a38c2849 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
@@ -1117,6 +1117,40 @@ public class TestMetadataObjectTagOperations extends 
BaseOperationsTest {
         response.readEntity(NameListResponse.class).getNames());
   }
 
+  @Test
+  public void testAssociateTagsForObjectWithNullRequest() {
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+
+    Response response =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(response);
+  }
+
+  @Test
+  public void testAssociateTagValuesForObjectWithNullRequest() {
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+
+    Response response =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v2+json")
+            .post(Entity.entity("null", "application/vnd.gravitino.v2+json"));
+
+    assertNullRequestBodyRejected(response);
+    Assertions.assertEquals(
+        MediaType.valueOf("application/vnd.gravitino.v2+json"), 
response.getMediaType());
+  }
+
   @Test
   public void testV2ErrorMediaType() {
     MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetalakeOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetalakeOperations.java
index ae7eb39133..a2cbd0b251 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetalakeOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetalakeOperations.java
@@ -51,6 +51,7 @@ import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.MetalakeChange;
 import org.apache.gravitino.dto.MetalakeDTO;
 import org.apache.gravitino.dto.requests.MetalakeCreateRequest;
+import org.apache.gravitino.dto.requests.MetalakeSetRequest;
 import org.apache.gravitino.dto.requests.MetalakeUpdateRequest;
 import org.apache.gravitino.dto.requests.MetalakeUpdatesRequest;
 import org.apache.gravitino.dto.responses.DropResponse;
@@ -70,6 +71,7 @@ import 
org.apache.gravitino.server.web.mapper.JsonMappingExceptionMapper;
 import org.apache.gravitino.server.web.mapper.JsonParseExceptionMapper;
 import org.apache.gravitino.server.web.mapper.JsonProcessingExceptionMapper;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.client.HttpUrlConnectorProvider;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.TestProperties;
 import org.junit.jupiter.api.Assertions;
@@ -263,6 +265,67 @@ public class TestMetalakeOperations extends 
BaseOperationsTest {
     assertNullRequestBodyRejected(resp);
   }
 
+  @Test
+  public void testSetMetalake() {
+    Mockito.doNothing().when(metalakeManager).enableMetalake(any());
+    Mockito.doNothing().when(metalakeManager).disableMetalake(any());
+
+    Response enableResp =
+        target("/metalakes/test")
+            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .method(
+                "PATCH",
+                Entity.entity(new MetalakeSetRequest(true), 
MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
enableResp.getStatus());
+    Mockito.verify(metalakeManager).enableMetalake(any());
+
+    Response disableResp =
+        target("/metalakes/test")
+            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .method(
+                "PATCH",
+                Entity.entity(new MetalakeSetRequest(false), 
MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
disableResp.getStatus());
+    Mockito.verify(metalakeManager).disableMetalake(any());
+  }
+
+  @Test
+  public void testSetMetalakeWithNullRequest() {
+    Response resp =
+        target("/metalakes/test")
+            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .method("PATCH", Entity.entity("null", 
MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testSetMetalakeWithMalformedJson() {
+    // The null-body guard must not take over malformed JSON, which keeps 
using the registered
+    // Jackson exception mappers.
+    Response resp =
+        target("/metalakes/test")
+            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .method("PATCH", Entity.entity("{", 
MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp.getStatus());
+    ErrorResponse errorResponse = resp.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, 
errorResponse.getCode());
+    Assertions.assertEquals(
+        IllegalArgumentException.class.getSimpleName(), 
errorResponse.getType());
+    Assertions.assertTrue(errorResponse.getMessage().contains("Malformed json 
request"));
+  }
+
   @Test
   public void testLoadMetalake() {
     String metalakeName = "test";
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestOwnerOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestOwnerOperations.java
index 97eb522374..b342f3cfda 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestOwnerOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestOwnerOperations.java
@@ -316,6 +316,17 @@ class TestOwnerOperations extends BaseOperationsTest {
     Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, 
errorResponse3.getCode());
   }
 
+  @Test
+  public void testSetOwnerForObjectWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/owners/metalake/metalake1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
   @Test
   public void testRoleObject() {
     MetadataObject role = MetadataObjects.of(null, "role", 
MetadataObject.Type.ROLE);
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
index 6a18db9af7..037490a407 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
@@ -812,4 +812,81 @@ public class TestPermissionOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResponse2.getCode());
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse2.getType());
   }
+
+  @Test
+  public void testGrantRolesToUserWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/permissions/users/user1/grant")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testGrantRolesToGroupWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/permissions/groups/group1/grant")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testRevokeRolesFromUserWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/permissions/users/user1/revoke")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testRevokeRolesFromGroupWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/permissions/groups/group1/revoke")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testGrantPrivilegeToRoleWithNullRequest() {
+    Response resp =
+        
target("/metalakes/metalake1/permissions/roles/role1/metalake/metalake1/grant")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testRevokePrivilegeFromRoleWithNullRequest() {
+    Response resp =
+        
target("/metalakes/metalake1/permissions/roles/role1/metalake/metalake1/revoke")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testOverrideRolePrivilegesWithNullRequest() {
+    Response resp =
+        target("/metalakes/metalake1/permissions/roles/role1/")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(new byte[0], MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
 }
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
index 3095512cac..85e45239fe 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
@@ -671,6 +671,19 @@ public class TestPolicyOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp.getType());
   }
 
+  @Test
+  public void testSetPolicyWithNullRequest() {
+    Response resp =
+        target(policyPath(metalake))
+            .path("policy1")
+            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .method("PATCH", Entity.entity("null", 
MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
   @Test
   public void testDeletePolicy() {
     when(policyManager.deletePolicy(metalake, "policy1")).thenReturn(true);
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestStatisticOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestStatisticOperations.java
index a970af8214..da591459de 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestStatisticOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestStatisticOperations.java
@@ -73,14 +73,13 @@ import org.apache.gravitino.stats.StatisticValue;
 import org.apache.gravitino.stats.StatisticValues;
 import org.glassfish.jersey.internal.inject.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
-import org.glassfish.jersey.test.JerseyTest;
 import org.glassfish.jersey.test.TestProperties;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
-public class TestStatisticOperations extends JerseyTest {
+public class TestStatisticOperations extends BaseOperationsTest {
 
   private static class MockServletRequestFactory extends 
ServletRequestFactoryBase {
     @Override
@@ -392,12 +391,80 @@ public class TestStatisticOperations extends JerseyTest {
             .accept("application/vnd.gravitino.v1+json")
             .put(entity("null", MediaType.APPLICATION_JSON_TYPE));
 
-    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+    assertNullRequestBodyRejected(resp);
+  }
 
-    ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
-    Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, 
errorResp.getCode());
-    Assertions.assertEquals(IllegalArgumentException.class.getSimpleName(), 
errorResp.getType());
+  @Test
+  public void testDropStatisticsWithNullRequest() {
+    when(tableDispatcher.tableExists(any())).thenReturn(true);
+
+    MetadataObject tableObject =
+        MetadataObjects.parse(
+            String.format("%s.%s.%s", catalog, schema, table), 
MetadataObject.Type.TABLE);
+
+    Response resp =
+        target(
+                "/metalakes/"
+                    + metalake
+                    + "/objects/"
+                    + tableObject.type()
+                    + "/"
+                    + tableObject.fullName()
+                    + "/statistics")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testUpdatePartitionStatisticsWithNullRequest() {
+    when(tableDispatcher.tableExists(any())).thenReturn(true);
+
+    MetadataObject tableObject =
+        MetadataObjects.parse(
+            String.format("%s.%s.%s", catalog, schema, table), 
MetadataObject.Type.TABLE);
+
+    Response resp =
+        target(
+                "/metalakes/"
+                    + metalake
+                    + "/objects/"
+                    + tableObject.type()
+                    + "/"
+                    + tableObject.fullName()
+                    + "/statistics/partitions")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
+  }
+
+  @Test
+  public void testDropPartitionStatisticsWithNullRequest() {
+    when(tableDispatcher.tableExists(any())).thenReturn(true);
+
+    MetadataObject tableObject =
+        MetadataObjects.parse(
+            String.format("%s.%s.%s", catalog, schema, table), 
MetadataObject.Type.TABLE);
+
+    Response resp =
+        target(
+                "/metalakes/"
+                    + metalake
+                    + "/objects/"
+                    + tableObject.type()
+                    + "/"
+                    + tableObject.fullName()
+                    + "/statistics/partitions")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(entity(new byte[0], MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(resp);
   }
 
   @Test
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
index 9a9fe3785f..c4672a1114 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
@@ -1167,6 +1167,22 @@ public class TestTagOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse1.getType());
   }
 
+  @Test
+  public void testAssociateTagsForObjectWithNullRequest() {
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+
+    // The deprecated route delegates to MetadataObjectTagOperations, so it 
inherits the same guard.
+    Response response =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(response);
+  }
+
   private String tagPath(String metalake) {
     return "/metalakes/" + metalake + "/tags";
   }

Reply via email to