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)
+ })
+})