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

yuqi1129 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 8e41cedffb [#12879] fix(server): return accurate HTTP statuses for 
unsupported operations (#12880)
8e41cedffb is described below

commit 8e41cedffb42064b240f93312e98610ee1788628
Author: Qi Yu <[email protected]>
AuthorDate: Fri Sep 4 17:08:39 2026 +0800

    [#12879] fix(server): return accurate HTTP statuses for unsupported 
operations (#12880)
    
    ### What changes were proposed in this pull request?
    
    - Classify REST failures by semantics at the common exception-handling
    boundary:
    - Return HTTP 400 when statistics endpoints receive a non-table object
    type.
    - Return HTTP 409 when an operation conflicts with object state,
    including unmodifiable statistics and non-empty IdP groups.
    - Return HTTP 501 for recognized but unsupported server or connector
    capabilities, including handlers that use the common fallback.
    - Preserve the authorization-disabled filter's existing HTTP 405
    contract through a dedicated helper instead of sharing the
    unsupported-capability helper.
    - Align the built-in IdP mapper so unsupported operations return 501 and
    unexpected `IllegalStateException` returns 500.
    - Keep the existing application error codes so Java clients continue
    reconstructing the same exceptions.
    - Update OpenAPI contracts and make Web v2 recognize unsupported
    operations by application error code, with HTTP 501 as a fallback, so it
    works with both old and new servers.
    
    ### Why are the changes needed?
    
    HTTP 405 means the request method is not supported by the target
    resource. The reported failures use valid registered methods and instead
    represent invalid input, resource conflicts, or unavailable
    functionality. The old mapping was also inconsistent because resource
    handlers without a dedicated branch returned HTTP 500 for the same
    unsupported-capability exception.
    
    The fix centralizes the fallback while classifying more specific domain
    exceptions before the generic `UnsupportedOperationException` branch.
    
    Fix: #12879
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Invalid statistics object types return HTTP 400, state conflicts
    return HTTP 409, and unsupported REST capabilities return HTTP 501.
    Response payload codes remain compatible. The authorization-disabled
    filter keeps its existing HTTP 405 behavior.
    
    ### How was this patch tested?
    
    - `./gradlew spotlessApply`
    - `./gradlew :server-common:test --tests
    org.apache.gravitino.server.web.TestUtils -PskipITs
    -PskipDockerTests=true`
    - `./gradlew :server:test --tests
    org.apache.gravitino.server.web.filter.TestAccessControlNotAllowedFilter
    --tests org.apache.gravitino.server.web.rest.TestExceptionHandlers
    --tests org.apache.gravitino.server.web.rest.TestStatisticOperations
    -PskipITs -PskipDockerTests=true`
    - `./gradlew :clients:client-java:test --tests
    org.apache.gravitino.client.TestSupportsStatistics --tests
    org.apache.gravitino.client.TestSupportsPartitionStatistics -PskipITs
    -PskipDockerTests=true`
    - `./gradlew :plugins:idp-basic:test --tests
    org.apache.gravitino.idp.web.TestIdpRESTUtils --tests
    org.apache.gravitino.idp.TestIdpUserGroupManager -PskipITs
    -PskipDockerTests=true`
    - `./gradlew :plugins:idp-basic:test --tests
    org.apache.gravitino.idp.integration.test.IdpRESTApiIT
    -PskipDockerTests=true`
    - `./gradlew :docs:build -PskipITs -PskipDockerTests=true`
    - `pnpm exec vitest run
    src/lib/utils/axios/unsupportedOperation.test.js`
    - `pnpm prettier:check`
    - `pnpm lint`
---
 .../gravitino/client/TestRelationalCatalog.java    |  4 +-
 .../client/TestSupportsPartitionStatistics.java    | 11 +--
 .../gravitino/client/TestSupportsStatistics.java   | 11 +--
 docs/open-api/idp/idp.yaml                         |  8 +-
 docs/open-api/statistics.yaml                      | 62 +++++++++++---
 .../idp/storage/service/IdpGroupMetaService.java   |  6 +-
 .../org/apache/gravitino/idp/web/IdpRESTUtils.java | 10 ++-
 .../gravitino/idp/TestIdpUserGroupManager.java     |  3 +-
 .../idp/integration/test/IdpRESTApiIT.java         |  2 +-
 .../storage/service/TestIdpGroupMetaService.java   |  3 +-
 .../apache/gravitino/idp/web/TestIdpRESTUtils.java | 70 ++++++++++++++++
 .../org/apache/gravitino/server/web/Utils.java     | 48 ++++++++++-
 .../org/apache/gravitino/server/web/TestUtils.java | 28 ++++++-
 .../web/filter/AccessControlNotAllowedFilter.java  |  2 +-
 .../server/web/rest/ExceptionHandlers.java         | 19 +++++
 .../server/web/rest/StatisticOperations.java       | 12 +--
 .../filter/TestAccessControlNotAllowedFilter.java  |  9 +-
 .../server/web/rest/TestExceptionHandlers.java     | 51 ++++++++++++
 .../server/web/rest/TestStatisticOperations.java   | 95 +++++++++++++++++++++-
 web-v2/web/src/lib/store/metalakes/index.js        |  9 +-
 .../src/lib/utils/axios/unsupportedOperation.js    | 22 ++---
 .../lib/utils/axios/unsupportedOperation.test.js   | 43 ++++++++++
 22 files changed, 451 insertions(+), 77 deletions(-)

diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestRelationalCatalog.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestRelationalCatalog.java
index b10e9f981e..64feaa121c 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestRelationalCatalog.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestRelationalCatalog.java
@@ -25,8 +25,8 @@ import static 
org.apache.gravitino.rel.expressions.sorts.SortDirection.DESCENDIN
 import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST;
 import static org.apache.hc.core5.http.HttpStatus.SC_CONFLICT;
 import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
-import static org.apache.hc.core5.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
 import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.hc.core5.http.HttpStatus.SC_NOT_IMPLEMENTED;
 import static org.apache.hc.core5.http.HttpStatus.SC_OK;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -1186,7 +1186,7 @@ public class TestRelationalCatalog extends TestBase {
 
     // Test with exception
     ErrorResponse errorResp = ErrorResponse.unsupportedOperation("Unsupported 
operation");
-    buildMockResource(Method.DELETE, tablePath, null, errorResp, 
SC_METHOD_NOT_ALLOWED);
+    buildMockResource(Method.DELETE, tablePath, null, errorResp, 
SC_NOT_IMPLEMENTED);
 
     TableCatalog tableCatalog = catalog.asTableCatalog();
     Assertions.assertThrows(
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsPartitionStatistics.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsPartitionStatistics.java
index d19aaed1a6..fca28cae0d 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsPartitionStatistics.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsPartitionStatistics.java
@@ -19,8 +19,8 @@
 package org.apache.gravitino.client;
 
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -209,12 +209,7 @@ public class TestSupportsPartitionStatistics extends 
TestBase {
                 Collections.emptyList()));
     ErrorResponse unmodifiableError = MAPPER.readValue(unmodifiableErrorJson, 
ErrorResponse.class);
     buildMockResource(
-        Method.PUT,
-        path,
-        Collections.emptyMap(),
-        expectedRequest,
-        unmodifiableError,
-        SC_METHOD_NOT_ALLOWED);
+        Method.PUT, path, Collections.emptyMap(), expectedRequest, 
unmodifiableError, SC_CONFLICT);
 
     Assertions.assertThrows(
         UnmodifiableStatisticException.class,
@@ -274,7 +269,7 @@ public class TestSupportsPartitionStatistics extends 
TestBase {
                 Collections.emptyList()));
     ErrorResponse unmodifiableError = MAPPER.readValue(unmodifiableErrorJson, 
ErrorResponse.class);
     buildMockResource(
-        Method.POST, path, Collections.emptyMap(), null, unmodifiableError, 
SC_METHOD_NOT_ALLOWED);
+        Method.POST, path, Collections.emptyMap(), null, unmodifiableError, 
SC_CONFLICT);
 
     Assertions.assertThrows(
         UnmodifiableStatisticException.class,
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsStatistics.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsStatistics.java
index ab1c1c1bee..c588deeb2f 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsStatistics.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsStatistics.java
@@ -19,8 +19,8 @@
 package org.apache.gravitino.client;
 
 import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.hc.core5.http.HttpStatus.SC_CONFLICT;
 import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
-import static org.apache.hc.core5.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
 import static org.apache.hc.core5.http.HttpStatus.SC_OK;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -184,12 +184,7 @@ public class TestSupportsStatistics extends TestBase {
                 Collections.emptyList()));
     ErrorResponse unmodifiableError = MAPPER.readValue(unmodifiableErrorJson, 
ErrorResponse.class);
     buildMockResource(
-        Method.PUT,
-        path,
-        Collections.emptyMap(),
-        expectedRequest,
-        unmodifiableError,
-        SC_METHOD_NOT_ALLOWED);
+        Method.PUT, path, Collections.emptyMap(), expectedRequest, 
unmodifiableError, SC_CONFLICT);
 
     Assertions.assertThrows(
         UnmodifiableStatisticException.class,
@@ -246,7 +241,7 @@ public class TestSupportsStatistics extends TestBase {
                 Collections.emptyList()));
     ErrorResponse unmodifiableError = MAPPER.readValue(unmodifiableErrorJson, 
ErrorResponse.class);
     buildMockResource(
-        Method.POST, path, Collections.emptyMap(), null, unmodifiableError, 
SC_METHOD_NOT_ALLOWED);
+        Method.POST, path, Collections.emptyMap(), null, unmodifiableError, 
SC_CONFLICT);
 
     Assertions.assertThrows(
         UnmodifiableStatisticException.class,
diff --git a/docs/open-api/idp/idp.yaml b/docs/open-api/idp/idp.yaml
index 42d4a05749..71e8ff581a 100644
--- a/docs/open-api/idp/idp.yaml
+++ b/docs/open-api/idp/idp.yaml
@@ -269,8 +269,8 @@ paths:
           $ref: "../openapi.yaml#/components/responses/RemoveResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
-        "405":
-          description: Method Not Allowed - The group is not empty and force 
is false
+        "409":
+          description: Conflict - The group is not empty and force is false
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -666,8 +666,8 @@ components:
 
     IdpGroupNotEmptyException:
       value: {
-        "code": 1006,
-        "type": "IllegalStateException",
+        "code": 1005,
+        "type": "NonEmptyEntityException",
         "message": "Failed to operate local user store group [engineers] 
operation [REMOVE], reason [IdP group engineers is not empty, use force=true to 
delete it]"
       }
 
diff --git a/docs/open-api/statistics.yaml b/docs/open-api/statistics.yaml
index 91e422be45..826a4b7ec1 100644
--- a/docs/open-api/statistics.yaml
+++ b/docs/open-api/statistics.yaml
@@ -41,6 +41,8 @@ paths:
                 examples:
                   StatisticListResponse:
                     $ref: "#/components/examples/StatisticListResponse"
+        "400":
+          $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
         "404":
           description: Not Found - The specified metadata object doesn't exist
           content:
@@ -50,6 +52,12 @@ paths:
               examples:
                 NoSuchMetadataObjectException:
                   $ref: "#/components/examples/NoSuchMetadataObjectException"
+        "501":
+          description: Not Implemented - Statistics are not supported for the 
specified object
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
         "5xx":
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -71,7 +79,7 @@ paths:
         "200":
           $ref: "./openapi.yaml#/components/responses/DropResponse"
         "400":
-          description: Bad Request - The request contains illegal statistic 
names
+          description: Bad Request - The object type or statistic names are 
invalid
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -88,8 +96,8 @@ paths:
               examples:
                 NoSuchMetadataObjectException:
                   $ref: "#/components/examples/NoSuchMetadataObjectException"
-        "405":
-          description: Method Not Allowed - The specified statistic is 
unmodifiable
+        "409":
+          description: Conflict - The specified statistic is unmodifiable
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -97,6 +105,12 @@ paths:
               examples:
                 UnmodifiableStatisticException:
                   $ref: "#/components/examples/UnmodifiableStatisticException"
+        "501":
+          description: Not Implemented - The requested statistics operation is 
not supported
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
         "5xx":
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -118,7 +132,7 @@ paths:
         "200":
           $ref: "./openapi.yaml#/components/responses/BaseResponse"
         "400":
-          description: Bad Request - The request contains illegal statistic 
names
+          description: Bad Request - The object type or statistic names are 
invalid
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -135,8 +149,8 @@ paths:
               examples:
                 NoSuchMetadataObjectException:
                   $ref: "#/components/examples/NoSuchMetadataObjectException"
-        "405":
-          description: Method Not Allowed - The specified statistic is 
unmodifiable
+        "409":
+          description: Conflict - The specified statistic is unmodifiable
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -144,6 +158,12 @@ paths:
               examples:
                 UnmodifiableStatisticException:
                   $ref: "#/components/examples/UnmodifiableStatisticException"
+        "501":
+          description: Not Implemented - The requested statistics operation is 
not supported
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
         "5xx":
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -184,6 +204,12 @@ paths:
               examples:
                 NoSuchMetadataObjectException:
                   $ref: "#/components/examples/NoSuchMetadataObjectException"
+        "501":
+          description: Not Implemented - Partition statistics are not 
supported for the specified object
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
         "5xx":
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -205,7 +231,7 @@ paths:
         "200":
           $ref: "./openapi.yaml#/components/responses/DropResponse"
         "400":
-          description: Bad Request - The request contains illegal statistic 
names
+          description: Bad Request - The object type or statistic names are 
invalid
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -222,8 +248,8 @@ paths:
               examples:
                 NoSuchMetadataObjectException:
                   $ref: "#/components/examples/NoSuchMetadataObjectException"
-        "405":
-          description: Method Not Allowed - The specified statistic is 
unmodifiable
+        "409":
+          description: Conflict - The specified statistic is unmodifiable
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -231,6 +257,12 @@ paths:
               examples:
                 UnmodifiableStatisticException:
                   $ref: "#/components/examples/UnmodifiableStatisticException"
+        "501":
+          description: Not Implemented - The requested partition statistics 
operation is not supported
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
         "5xx":
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -252,7 +284,7 @@ paths:
         "200":
           $ref: "./openapi.yaml#/components/responses/BaseResponse"
         "400":
-          description: Bad Request - The request contains illegal statistic 
names
+          description: Bad Request - The object type or statistic names are 
invalid
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -269,8 +301,8 @@ paths:
               examples:
                 NoSuchMetadataObjectException:
                   $ref: "#/components/examples/NoSuchMetadataObjectException"
-        "405":
-          description: Method Not Allowed - The specified statistic is 
unmodifiable
+        "409":
+          description: Conflict - The specified statistic is unmodifiable
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -278,6 +310,12 @@ paths:
               examples:
                 UnmodifiableStatisticException:
                   $ref: "#/components/examples/UnmodifiableStatisticException"
+        "501":
+          description: Not Implemented - The requested partition statistics 
operation is not supported
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
         "5xx":
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
index c4bfdd3601..018b20fe1a 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.apache.gravitino.exceptions.NonEmptyEntityException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.idp.model.IdpGroup;
 import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper;
@@ -104,14 +105,15 @@ public class IdpGroupMetaService {
    * @param force when false, rejects deletion if the group still has members; 
when true, removes
    *     memberships and deletes the group
    * @return true if the group was deleted
+   * @throws NonEmptyEntityException if the group has members and force is 
false
    */
   @Monitored(
       metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
       baseMetricName = "deleteIdpGroup")
   public boolean deleteIdpGroup(String groupName, boolean force) {
     if (!force && !listUsernamesByGroupName(groupName).isEmpty()) {
-      throw new IllegalStateException(
-          String.format("IdP group %s is not empty, use force=true to delete 
it", groupName));
+      throw new NonEmptyEntityException(
+          "IdP group %s is not empty, use force=true to delete it", groupName);
     }
 
     int[] deletedCount = new int[] {0};
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
index 75ab056738..95aaa1b9fd 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.UserPrincipal;
 import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.exceptions.AlreadyExistsException;
+import org.apache.gravitino.exceptions.NonEmptyEntityException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.utils.PrincipalUtils;
 import org.slf4j.Logger;
@@ -103,7 +104,7 @@ public final class IdpRESTUtils {
 
   public static Response unsupportedOperation(String message, Throwable 
throwable) {
     return json(
-        Response.Status.METHOD_NOT_ALLOWED, 
ErrorResponse.unsupportedOperation(message, throwable));
+        Response.Status.NOT_IMPLEMENTED, 
ErrorResponse.unsupportedOperation(message, throwable));
   }
 
   public static Response internalError(String message, Throwable throwable) {
@@ -121,7 +122,12 @@ public final class IdpRESTUtils {
     if (e instanceof AlreadyExistsException) {
       return alreadyExists(errorMsg, e);
     }
-    if (e instanceof IllegalStateException || e instanceof 
UnsupportedOperationException) {
+    if (e instanceof NonEmptyEntityException) {
+      return json(
+          Response.Status.CONFLICT,
+          ErrorResponse.nonEmpty(e.getClass().getSimpleName(), errorMsg, e));
+    }
+    if (e instanceof UnsupportedOperationException) {
       return unsupportedOperation(errorMsg, e);
     }
     return internalError(errorMsg, e);
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
index 5a1fae591f..0a7a835654 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
@@ -40,6 +40,7 @@ import java.util.stream.Stream;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.auth.AuthenticatorType;
 import org.apache.gravitino.exceptions.AlreadyExistsException;
+import org.apache.gravitino.exceptions.NonEmptyEntityException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.exceptions.UnauthorizedException;
 import org.apache.gravitino.idp.basic.IdpCredentialValidator;
@@ -256,7 +257,7 @@ public class TestIdpUserGroupManager {
     manager.changeGroupMembership("testRemoveGroup", 
Lists.newArrayList("groupMember"), null);
 
     Assertions.assertThrows(
-        IllegalStateException.class, () -> 
manager.removeGroup("testRemoveGroup", false));
+        NonEmptyEntityException.class, () -> 
manager.removeGroup("testRemoveGroup", false));
 
     manager.changeGroupMembership("testRemoveGroup", null, 
Lists.newArrayList("groupMember"));
     Assertions.assertTrue(manager.removeGroup("testRemoveGroup", false));
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
index 026dfca21d..bf970fe1af 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
@@ -228,7 +228,7 @@ public class IdpRESTApiIT extends BaseIT {
         putMembership(GROUP1, new GroupMembershipChangeRequest(new String[] 
{USER1, USER2}, null));
     Assertions.assertEquals(Set.of(USER1, USER2), 
Set.copyOf(group.getGroup().users()));
 
-    assertError(405, deleteGroupResponse(GROUP1, false), 
ErrorConstants.UNSUPPORTED_OPERATION_CODE);
+    assertError(409, deleteGroupResponse(GROUP1, false), 
ErrorConstants.NON_EMPTY_CODE);
 
     group =
         putMembership(GROUP1, new GroupMembershipChangeRequest(null, new 
String[] {USER1, USER2}));
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
index b0b22e7f21..70067d66ac 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
@@ -28,6 +28,7 @@ import java.io.IOException;
 import java.util.List;
 import java.util.Set;
 import org.apache.gravitino.exceptions.AlreadyExistsException;
+import org.apache.gravitino.exceptions.NonEmptyEntityException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.idp.storage.po.IdpGroupPO;
 import org.junit.jupiter.api.Tag;
@@ -121,7 +122,7 @@ class TestIdpGroupMetaService extends 
AbstractIdpMetaServiceTest {
     IdpGroupMetaService groupMetaService = IdpGroupMetaService.getInstance();
 
     assertThrows(
-        IllegalStateException.class, () -> 
groupMetaService.deleteIdpGroup("group1", false));
+        NonEmptyEntityException.class, () -> 
groupMetaService.deleteIdpGroup("group1", false));
 
     runServiceCall(() -> assertTrue(groupMetaService.deleteIdpGroup("group1", 
true)));
     assertNull(idpGroupMetaMapper.selectIdpGroup("group1"));
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/TestIdpRESTUtils.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/TestIdpRESTUtils.java
new file mode 100644
index 0000000000..3b8840fd14
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/TestIdpRESTUtils.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.idp.web;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.exceptions.NonEmptyEntityException;
+import org.junit.jupiter.api.Test;
+
+class TestIdpRESTUtils {
+
+  @Test
+  void testUnsupportedOperationReturnsNotImplemented() {
+    Response response =
+        IdpRESTUtils.handleException(
+            "group",
+            IdpOperationType.GET,
+            "group",
+            new UnsupportedOperationException("unsupported"));
+
+    assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), 
response.getStatus());
+    ErrorResponse error = (ErrorResponse) response.getEntity();
+    assertEquals(ErrorConstants.UNSUPPORTED_OPERATION_CODE, error.getCode());
+  }
+
+  @Test
+  void testNonEmptyEntityReturnsConflict() {
+    Response response =
+        IdpRESTUtils.handleException(
+            "group",
+            IdpOperationType.REMOVE,
+            "group",
+            new NonEmptyEntityException("Group is not empty"));
+
+    assertEquals(Response.Status.CONFLICT.getStatusCode(), 
response.getStatus());
+    ErrorResponse error = (ErrorResponse) response.getEntity();
+    assertEquals(ErrorConstants.NON_EMPTY_CODE, error.getCode());
+    assertEquals(NonEmptyEntityException.class.getSimpleName(), 
error.getType());
+  }
+
+  @Test
+  void testUnexpectedIllegalStateReturnsInternalError() {
+    Response response =
+        IdpRESTUtils.handleException(
+            "group", IdpOperationType.GET, "group", new 
IllegalStateException("invalid state"));
+
+    assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response.getStatus());
+    ErrorResponse error = (ErrorResponse) response.getEntity();
+    assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, error.getCode());
+  }
+}
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java 
b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
index 21338047d6..e2165637bb 100644
--- a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
+++ b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
@@ -189,17 +189,63 @@ public class Utils {
         .build();
   }
 
+  /**
+   * Returns an HTTP 501 response for functionality that the server does not 
implement.
+   *
+   * @param message the error message
+   * @return the HTTP response
+   */
   public static Response unsupportedOperation(String message) {
     return unsupportedOperation(message, null);
   }
 
+  /**
+   * Returns an HTTP 501 response for functionality that the server does not 
implement.
+   *
+   * @param message the error message
+   * @param throwable the exception that caused the error
+   * @return the HTTP response
+   */
   public static Response unsupportedOperation(String message, Throwable 
throwable) {
-    return Response.status(Response.Status.METHOD_NOT_ALLOWED)
+    return Response.status(Response.Status.NOT_IMPLEMENTED)
+        .entity(ErrorResponse.unsupportedOperation(message, throwable))
+        .type(MediaType.APPLICATION_JSON)
+        .build();
+  }
+
+  /**
+   * Returns an HTTP 409 response when an operation conflicts with the target 
object's state.
+   *
+   * <p>The unsupported-operation error payload is retained so existing 
clients can reconstruct
+   * domain exceptions such as {@code UnmodifiableStatisticException}.
+   *
+   * @param message the error message
+   * @param throwable the exception that caused the error
+   * @return the HTTP response
+   */
+  public static Response operationConflict(String message, Throwable 
throwable) {
+    return Response.status(Response.Status.CONFLICT)
         .entity(ErrorResponse.unsupportedOperation(message, throwable))
         .type(MediaType.APPLICATION_JSON)
         .build();
   }
 
+  /**
+   * Returns an HTTP 405 response when the target resource does not allow the 
request method.
+   *
+   * <p>The unsupported-operation error payload is retained for compatibility 
with clients that
+   * identify this response by its application error code.
+   *
+   * @param message the error message
+   * @return the HTTP response
+   */
+  public static Response methodNotAllowed(String message) {
+    return Response.status(Response.Status.METHOD_NOT_ALLOWED)
+        .entity(ErrorResponse.unsupportedOperation(message))
+        .type(MediaType.APPLICATION_JSON)
+        .build();
+  }
+
   public static Response forbidden(String message, Throwable throwable) {
     return Response.status(Response.Status.FORBIDDEN)
         .entity(ErrorResponse.forbidden(message, throwable))
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java 
b/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
index 3f276c8316..d9384a013d 100644
--- a/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
+++ b/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
@@ -33,6 +33,7 @@ import org.apache.gravitino.audit.InternalClientType;
 import org.apache.gravitino.dto.responses.ErrorConstants;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.exceptions.OptimisticLockException;
+import org.apache.gravitino.exceptions.UnmodifiableStatisticException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
@@ -184,12 +185,37 @@ public class TestUtils {
   public void testUnsupportedOperation() {
     Response response = Utils.unsupportedOperation("Unsupported operation");
     assertNotNull(response);
-    assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), 
response.getStatus());
+    assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), 
response.getStatus());
     assertEquals(MediaType.APPLICATION_JSON, 
response.getMediaType().toString());
     ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
     assertEquals("Unsupported operation", errorResponse.getMessage());
   }
 
+  @Test
+  public void testOperationConflict() {
+    UnmodifiableStatisticException exception =
+        new UnmodifiableStatisticException("Unmodifiable statistic");
+    Response response = Utils.operationConflict(exception.getMessage(), 
exception);
+
+    assertNotNull(response);
+    assertEquals(Response.Status.CONFLICT.getStatusCode(), 
response.getStatus());
+    assertEquals(MediaType.APPLICATION_JSON, 
response.getMediaType().toString());
+    ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
+    assertEquals(ErrorConstants.UNSUPPORTED_OPERATION_CODE, 
errorResponse.getCode());
+    assertEquals(UnmodifiableStatisticException.class.getSimpleName(), 
errorResponse.getType());
+  }
+
+  @Test
+  public void testMethodNotAllowed() {
+    Response response = Utils.methodNotAllowed("Method not allowed");
+
+    assertNotNull(response);
+    assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), 
response.getStatus());
+    assertEquals(MediaType.APPLICATION_JSON, 
response.getMediaType().toString());
+    ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
+    assertEquals(ErrorConstants.UNSUPPORTED_OPERATION_CODE, 
errorResponse.getCode());
+  }
+
   @Test
   public void testFilterFilesetAuditHeaders() {
     // test invalid internal client type
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/AccessControlNotAllowedFilter.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/AccessControlNotAllowedFilter.java
index 42c001f766..9a4b6761e2 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/AccessControlNotAllowedFilter.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/AccessControlNotAllowedFilter.java
@@ -40,7 +40,7 @@ public class AccessControlNotAllowedFilter implements 
ContainerRequestFilter {
   @Override
   public void filter(ContainerRequestContext requestContext) throws 
IOException {
     requestContext.abortWith(
-        Utils.unsupportedOperation(
+        Utils.methodNotAllowed(
             String.format(
                 "You should set '%s' to true in the server side 
`gravitino.conf`"
                     + " to enable the authorization of the system, otherwise 
these interfaces can't work.",
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 9ceb56f82b..471e1acecd 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -53,6 +53,7 @@ import 
org.apache.gravitino.exceptions.TableAlreadyExistsException;
 import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
 import org.apache.gravitino.exceptions.TagAlreadyExistsException;
 import org.apache.gravitino.exceptions.TopicAlreadyExistsException;
+import org.apache.gravitino.exceptions.UnmodifiableStatisticException;
 import org.apache.gravitino.exceptions.UserAlreadyExistsException;
 import org.apache.gravitino.exceptions.ViewAlreadyExistsException;
 import org.apache.gravitino.server.web.Utils;
@@ -1088,6 +1089,9 @@ public class ExceptionHandlers {
       } else if (e instanceof NotFoundException) {
         return Utils.notFound(errorMsg, e);
 
+      } else if (e instanceof UnmodifiableStatisticException) {
+        return Utils.operationConflict(errorMsg, e);
+
       } else if (e instanceof UnsupportedOperationException) {
         return Utils.unsupportedOperation(errorMsg, e);
 
@@ -1120,6 +1124,9 @@ public class ExceptionHandlers {
       } else if (e instanceof NotFoundException) {
         return Utils.notFound(errorMsg, e);
 
+      } else if (e instanceof UnmodifiableStatisticException) {
+        return Utils.operationConflict(errorMsg, e);
+
       } else if (e instanceof UnsupportedOperationException) {
         return Utils.unsupportedOperation(errorMsg, e);
 
@@ -1163,6 +1170,18 @@ public class ExceptionHandlers {
         return Utils.optimisticLockConflict(errorMsg, e);
       }
 
+      // Classify domain-specific UnsupportedOperationException subclasses 
before the generic
+      // capability fallback below.
+      if (e instanceof UnmodifiableStatisticException) {
+        LOG.warn(errorMsg, e);
+        return Utils.operationConflict(errorMsg, e);
+      }
+
+      if (e instanceof UnsupportedOperationException) {
+        LOG.warn(errorMsg, e);
+        return Utils.unsupportedOperation(errorMsg, e);
+      }
+
       LOG.error(errorMsg, e);
       return Utils.internalError(errorMsg, e);
     }
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 1c11abb958..03801348cb 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
@@ -119,7 +119,7 @@ public class StatisticOperations {
                 MetadataObjects.parse(
                     fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
             if (object.type() != MetadataObject.Type.TABLE) {
-              throw new UnsupportedOperationException(
+              throw new IllegalArgumentException(
                   "Listing statistics is only supported for tables now.");
             }
 
@@ -171,7 +171,7 @@ public class StatisticOperations {
                 MetadataObjects.parse(
                     fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
             if (object.type() != MetadataObject.Type.TABLE) {
-              throw new UnsupportedOperationException(
+              throw new IllegalArgumentException(
                   "Update statistics is only supported for tables now.");
             }
 
@@ -230,7 +230,7 @@ public class StatisticOperations {
                 MetadataObjects.parse(
                     fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
             if (object.type() != MetadataObject.Type.TABLE) {
-              throw new UnsupportedOperationException(
+              throw new IllegalArgumentException(
                   "Dropping statistics is only supported for tables now.");
             }
 
@@ -283,7 +283,7 @@ public class StatisticOperations {
                 MetadataObjects.parse(
                     fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
             if (object.type() != MetadataObject.Type.TABLE) {
-              throw new UnsupportedOperationException(
+              throw new IllegalArgumentException(
                   "Listing partition statistics is only supported for tables 
now.");
             }
 
@@ -368,7 +368,7 @@ public class StatisticOperations {
                 MetadataObjects.parse(
                     fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
             if (object.type() != MetadataObject.Type.TABLE) {
-              throw new UnsupportedOperationException(
+              throw new IllegalArgumentException(
                   "Updating partition statistics is only supported for tables 
now.");
             }
 
@@ -435,7 +435,7 @@ public class StatisticOperations {
                 MetadataObjects.parse(
                     fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
             if (object.type() != MetadataObject.Type.TABLE) {
-              throw new UnsupportedOperationException(
+              throw new IllegalArgumentException(
                   "Dropping partition statistics is only supported for tables 
now.");
             }
 
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestAccessControlNotAllowedFilter.java
 
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestAccessControlNotAllowedFilter.java
index 54b3f3c669..178884a3f2 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestAccessControlNotAllowedFilter.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestAccessControlNotAllowedFilter.java
@@ -18,13 +18,15 @@
  */
 package org.apache.gravitino.server.web.filter;
 
-import static org.mockito.ArgumentMatchers.any;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import java.io.IOException;
 import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.Response;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 
 public class TestAccessControlNotAllowedFilter {
   @Test
@@ -32,6 +34,9 @@ public class TestAccessControlNotAllowedFilter {
     AccessControlNotAllowedFilter filter = new AccessControlNotAllowedFilter();
     ContainerRequestContext requestContext = 
mock(ContainerRequestContext.class);
     filter.filter(requestContext);
-    verify(requestContext).abortWith(any());
+    ArgumentCaptor<Response> responseCaptor = 
ArgumentCaptor.forClass(Response.class);
+    verify(requestContext).abortWith(responseCaptor.capture());
+    assertEquals(
+        Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), 
responseCaptor.getValue().getStatus());
   }
 }
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
index ae5401af36..07d56d6ea8 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
@@ -18,10 +18,12 @@
  */
 package org.apache.gravitino.server.web.rest;
 
+import java.util.List;
 import javax.ws.rs.core.Response;
 import org.apache.gravitino.dto.responses.ErrorConstants;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.exceptions.OptimisticLockException;
+import org.apache.gravitino.exceptions.UnmodifiableStatisticException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -69,4 +71,53 @@ public class TestExceptionHandlers {
     Assertions.assertEquals(ErrorConstants.OPTIMISTIC_LOCK_CONFLICT_CODE, 
errorResponse.getCode());
     Assertions.assertEquals(OptimisticLockException.class.getSimpleName(), 
errorResponse.getType());
   }
+
+  @Test
+  void testUnsupportedOperationReturnsNotImplemented() {
+    UnsupportedOperationException exception =
+        new UnsupportedOperationException("Operation is not supported");
+    List<Response> responses =
+        List.of(
+            ExceptionHandlers.handleTableException(
+                OperationType.ALTER, "table", "schema", exception),
+            ExceptionHandlers.handlePolicyException(
+                OperationType.LIST, "policy", "metalake", exception),
+            new ExceptionHandlers.BaseExceptionHandler()
+                .handle(OperationType.LIST, "object", "parent", exception));
+
+    responses.forEach(
+        response -> {
+          Assertions.assertEquals(
+              Response.Status.NOT_IMPLEMENTED.getStatusCode(), 
response.getStatus());
+          ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
+          Assertions.assertEquals(
+              ErrorConstants.UNSUPPORTED_OPERATION_CODE, 
errorResponse.getCode());
+          Assertions.assertEquals(
+              UnsupportedOperationException.class.getSimpleName(), 
errorResponse.getType());
+        });
+  }
+
+  @Test
+  void testUnmodifiableOperationReturnsConflict() {
+    UnmodifiableStatisticException exception =
+        new UnmodifiableStatisticException("Statistic is unmodifiable");
+    List<Response> responses =
+        List.of(
+            ExceptionHandlers.handleStatisticException(
+                OperationType.ALTER, "statistic", "table", exception),
+            ExceptionHandlers.handlePartitionStatsException(
+                OperationType.ALTER, "partition", "table", exception),
+            new ExceptionHandlers.BaseExceptionHandler()
+                .handle(OperationType.ALTER, "statistic", "table", exception));
+
+    responses.forEach(
+        response -> {
+          Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
response.getStatus());
+          ErrorResponse errorResponse = (ErrorResponse) response.getEntity();
+          Assertions.assertEquals(
+              ErrorConstants.UNSUPPORTED_OPERATION_CODE, 
errorResponse.getCode());
+          Assertions.assertEquals(
+              UnmodifiableStatisticException.class.getSimpleName(), 
errorResponse.getType());
+        });
+  }
 }
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 f8951a9966..751ee690c2 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
@@ -236,6 +236,93 @@ public class TestStatisticOperations extends 
BaseOperationsTest {
     ErrorResponse errorResp2 = resp2.readEntity(ErrorResponse.class);
     Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp2.getCode());
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp2.getType());
+
+    // Test throw UnsupportedOperationException
+    doThrow(new UnsupportedOperationException("mock error"))
+        .when(manager)
+        .listStatistics(any(), any());
+    Response resp3 =
+        target(
+                "/metalakes/"
+                    + metalake
+                    + "/objects/"
+                    + tableObject.type()
+                    + "/"
+                    + tableObject.fullName()
+                    + "/statistics")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), 
resp3.getStatus());
+    ErrorResponse errorResp3 = resp3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.UNSUPPORTED_OPERATION_CODE, 
errorResp3.getCode());
+    Assertions.assertEquals(
+        UnsupportedOperationException.class.getSimpleName(), 
errorResp3.getType());
+  }
+
+  /** Tests that every statistics endpoint rejects non-table object types as 
invalid arguments. */
+  @Test
+  public void testStatisticsEndpointsRejectNonTableObject() {
+    MetadataObject catalogObject = MetadataObjects.parse(catalog, 
MetadataObject.Type.CATALOG);
+    String path =
+        "/metalakes/"
+            + metalake
+            + "/objects/"
+            + catalogObject.type()
+            + "/"
+            + catalogObject.fullName()
+            + "/statistics";
+    Map<String, StatisticValue<?>> statistics =
+        Map.of(Statistic.CUSTOM_PREFIX + "test", 
StatisticValues.longValue(1L));
+    StatisticsUpdateRequest updateRequest = new 
StatisticsUpdateRequest(statistics);
+    StatisticsDropRequest dropRequest =
+        new StatisticsDropRequest(new String[] {Statistic.CUSTOM_PREFIX + 
"test"});
+    PartitionStatisticsUpdateRequest partitionUpdateRequest =
+        new PartitionStatisticsUpdateRequest(
+            List.of(PartitionStatisticsUpdateDTO.of("partition", statistics)));
+    PartitionStatisticsDropRequest partitionDropRequest =
+        new PartitionStatisticsDropRequest(
+            List.of(
+                PartitionStatisticsDropDTO.of(
+                    "partition", List.of(Statistic.CUSTOM_PREFIX + "test"))));
+
+    List<Response> responses =
+        List.of(
+            target(path)
+                .request(MediaType.APPLICATION_JSON_TYPE)
+                .accept("application/vnd.gravitino.v1+json")
+                .get(),
+            target(path)
+                .request(MediaType.APPLICATION_JSON_TYPE)
+                .accept("application/vnd.gravitino.v1+json")
+                .put(entity(updateRequest, MediaType.APPLICATION_JSON_TYPE)),
+            target(path)
+                .request(MediaType.APPLICATION_JSON_TYPE)
+                .accept("application/vnd.gravitino.v1+json")
+                .post(entity(dropRequest, MediaType.APPLICATION_JSON_TYPE)),
+            target(path + "/partitions")
+                .request(MediaType.APPLICATION_JSON_TYPE)
+                .accept("application/vnd.gravitino.v1+json")
+                .get(),
+            target(path + "/partitions")
+                .request(MediaType.APPLICATION_JSON_TYPE)
+                .accept("application/vnd.gravitino.v1+json")
+                .put(entity(partitionUpdateRequest, 
MediaType.APPLICATION_JSON_TYPE)),
+            target(path + "/partitions")
+                .request(MediaType.APPLICATION_JSON_TYPE)
+                .accept("application/vnd.gravitino.v1+json")
+                .post(entity(partitionDropRequest, 
MediaType.APPLICATION_JSON_TYPE)));
+
+    responses.forEach(
+        response -> {
+          Assertions.assertEquals(
+              Response.Status.BAD_REQUEST.getStatusCode(), 
response.getStatus());
+          ErrorResponse errorResponse = 
response.readEntity(ErrorResponse.class);
+          Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, 
errorResponse.getCode());
+          Assertions.assertEquals(
+              IllegalArgumentException.class.getSimpleName(), 
errorResponse.getType());
+        });
   }
 
   @Test
@@ -363,7 +450,7 @@ public class TestStatisticOperations extends 
BaseOperationsTest {
             .accept("application/vnd.gravitino.v1+json")
             .put(entity(req, MediaType.APPLICATION_JSON_TYPE));
 
-    
Assertions.assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), 
resp4.getStatus());
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp4.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp4.getMediaType());
 
     ErrorResponse errorResp4 = resp4.readEntity(ErrorResponse.class);
@@ -619,7 +706,7 @@ public class TestStatisticOperations extends 
BaseOperationsTest {
             .accept("application/vnd.gravitino.v1+json")
             .post(entity(req, MediaType.APPLICATION_JSON_TYPE));
 
-    
Assertions.assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), 
resp3.getStatus());
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp3.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp3.getMediaType());
 
     ErrorResponse errorResp3 = resp3.readEntity(ErrorResponse.class);
@@ -878,7 +965,7 @@ public class TestStatisticOperations extends 
BaseOperationsTest {
             .accept("application/vnd.gravitino.v1+json")
             .put(entity(req, MediaType.APPLICATION_JSON_TYPE));
 
-    
Assertions.assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), 
resp4.getStatus());
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp4.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp4.getMediaType());
 
     ErrorResponse errorResp4 = resp4.readEntity(ErrorResponse.class);
@@ -1050,7 +1137,7 @@ public class TestStatisticOperations extends 
BaseOperationsTest {
             .accept("application/vnd.gravitino.v1+json")
             .post(entity(req, MediaType.APPLICATION_JSON_TYPE));
 
-    
Assertions.assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), 
resp3.getStatus());
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp3.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp3.getMediaType());
 
     ErrorResponse errorResp3 = resp3.readEntity(ErrorResponse.class);
diff --git a/web-v2/web/src/lib/store/metalakes/index.js 
b/web-v2/web/src/lib/store/metalakes/index.js
index 55566d6973..49283333f9 100644
--- a/web-v2/web/src/lib/store/metalakes/index.js
+++ b/web-v2/web/src/lib/store/metalakes/index.js
@@ -20,6 +20,7 @@
 import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
 
 import { to, extractPlaceholder, updateTreeData, findInTree } from 
'@/lib/utils'
+import { isUnsupportedOperationError } from 
'@/lib/utils/axios/unsupportedOperation'
 import toast from 'react-hot-toast'
 
 import _ from 'lodash-es'
@@ -2250,8 +2251,8 @@ export const fetchViews = createAsyncThunk(
     const [err, res] = await to(getViewsApi({ metalake, catalog, schema }, { 
errorMessageMode: 'none' }))
 
     if (err || !res) {
-      // Catalog doesn't support views (HTTP 405) — return empty views silently
-      if (err?.response?.status === 405) {
+      // Catalog doesn't support views (HTTP 501, or legacy 405 with code 
1006) — return empty views silently
+      if (isUnsupportedOperationError(err)) {
         return { views: [], init }
       }
       if (init) {
@@ -2304,8 +2305,8 @@ export const getViewDetails = createAsyncThunk(
     const [err, res] = await to(getViewDetailsApi({ metalake, catalog, schema, 
view }, { errorMessageMode: 'none' }))
 
     if (err || !res) {
-      // Catalog doesn't support views (HTTP 405) — return empty result 
silently
-      if (err?.response?.status === 405) {
+      // Catalog doesn't support views (HTTP 501, or legacy 405 with code 
1006) — return empty result silently
+      if (isUnsupportedOperationError(err)) {
         return { view: null, init }
       }
       throw new Error(err)
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestAccessControlNotAllowedFilter.java
 b/web-v2/web/src/lib/utils/axios/unsupportedOperation.js
similarity index 54%
copy from 
server/src/test/java/org/apache/gravitino/server/web/filter/TestAccessControlNotAllowedFilter.java
copy to web-v2/web/src/lib/utils/axios/unsupportedOperation.js
index 54b3f3c669..bc9fb5c616 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestAccessControlNotAllowedFilter.java
+++ b/web-v2/web/src/lib/utils/axios/unsupportedOperation.js
@@ -16,22 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.server.web.filter;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
+const UNSUPPORTED_OPERATION_CODE = 1006
 
-import java.io.IOException;
-import javax.ws.rs.container.ContainerRequestContext;
-import org.junit.jupiter.api.Test;
-
-public class TestAccessControlNotAllowedFilter {
-  @Test
-  public void testAccessControlNotAllowedFilter() throws IOException {
-    AccessControlNotAllowedFilter filter = new AccessControlNotAllowedFilter();
-    ContainerRequestContext requestContext = 
mock(ContainerRequestContext.class);
-    filter.filter(requestContext);
-    verify(requestContext).abortWith(any());
-  }
-}
+/** Returns whether an HTTP client error represents an unsupported server 
operation. */
+export const isUnsupportedOperationError = error =>
+  error?.response?.status === 501 ||
+  (error?.response?.status === 405 && error?.response?.data?.code === 
UNSUPPORTED_OPERATION_CODE)
diff --git a/web-v2/web/src/lib/utils/axios/unsupportedOperation.test.js 
b/web-v2/web/src/lib/utils/axios/unsupportedOperation.test.js
new file mode 100644
index 0000000000..d8ef382d9b
--- /dev/null
+++ b/web-v2/web/src/lib/utils/axios/unsupportedOperation.test.js
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { describe, expect, it } from 'vitest'
+import { isUnsupportedOperationError } from 
'@/lib/utils/axios/unsupportedOperation'
+
+describe('isUnsupportedOperationError', () => {
+  it('recognizes HTTP 501 as an unsupported operation', () => {
+    expect(isUnsupportedOperationError({ response: { status: 501 } 
})).toBe(true)
+  })
+
+  it('recognizes the legacy HTTP 405 response by its application error code', 
() => {
+    expect(isUnsupportedOperationError({ response: { status: 405, data: { 
code: 1006 } } })).toBe(true)
+  })
+
+  it('does not hide an actual HTTP method mismatch', () => {
+    expect(isUnsupportedOperationError({ response: { status: 405, data: { 
code: 1000 } } })).toBe(false)
+  })
+
+  it('does not treat an HTTP conflict as an unsupported operation', () => {
+    expect(isUnsupportedOperationError({ response: { status: 409, data: { 
code: 1006 } } })).toBe(false)
+  })
+
+  it('handles errors without an HTTP response', () => {
+    expect(isUnsupportedOperationError(new Error('network error'))).toBe(false)
+  })
+})

Reply via email to