This is an automated email from the ASF dual-hosted git repository.
roryqi 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 a0c3e63503 [#13686] fix(iceberg-rest): Preserve
WebApplicationException HTTP status (#13687)
a0c3e63503 is described below
commit a0c3e635039a27e14d3e02376e5c9166ac33c1c5
Author: MaSai <[email protected]>
AuthorDate: Thu Oct 8 16:06:33 2026 +0800
[#13686] fix(iceberg-rest): Preserve WebApplicationException HTTP status
(#13687)
### What changes were proposed in this pull request?
- In `IcebergExceptionMapper`, preserve the HTTP status from JAX-RS
`WebApplicationException` instead of defaulting unmapped exceptions to
500.
- Keep `WebApplicationException` instances in
`convertToIcebergException` so auth paths that call `getErrorCode` stay
consistent.
- Add unit coverage for 406/405/415 and an Iceberg config integration
test with `Accept: application/vnd.gravitino.v1+json`.
### Why are the changes needed?
Content-negotiation failures are client errors (406), not server
failures (500). Returning 500 misleads clients and monitoring.
Fix: #13686
### Does this PR introduce _any_ user-facing change?
Yes. Iceberg REST responses for unacceptable `Accept` / unsupported
media type / disallowed method now return the correct HTTP status and
matching Iceberg `error.code` (e.g. 406), instead of 500.
### How was this patch tested?
- `./gradlew :iceberg:iceberg-rest-server:test --tests
org.apache.gravitino.iceberg.service.TestIcebergExceptionMapper --tests
org.apache.gravitino.iceberg.service.rest.TestIcebergConfig -PskipITs`
---------
Co-authored-by: Cursor <[email protected]>
---
.../iceberg/service/IcebergExceptionMapper.java | 28 ++++++++++++++++------
.../service/TestIcebergExceptionMapper.java | 24 +++++++++++++++++++
.../iceberg/service/rest/TestIcebergConfig.java | 19 +++++++++++++++
.../rest/TestIcebergNamespaceOperations.java | 5 ++--
4 files changed, 67 insertions(+), 9 deletions(-)
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
index b22fe5b407..913dad38d9 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.iceberg.service;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import javax.ws.rs.NotFoundException;
+import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
@@ -86,14 +87,26 @@ public class IcebergExceptionMapper implements
ExceptionMapper<Throwable> {
.build();
/**
- * Returns the HTTP status code for the given exception based on the Iceberg
REST spec.
+ * Returns the HTTP status code for the given exception.
+ *
+ * <ol>
+ * <li>Iceberg / Gravitino business exceptions from {@link
#EXCEPTION_ERROR_CODES}
+ * <li>JAX-RS {@link WebApplicationException} that already carries an HTTP
status
+ * <li>Unexpected failures default to 500
+ * </ol>
*
* @param ex the exception
- * @return the HTTP status code, defaulting to 500 for unmapped exceptions
+ * @return the HTTP status code
*/
public static int getErrorCode(Exception ex) {
- return EXCEPTION_ERROR_CODES.getOrDefault(
- ex.getClass(), Status.INTERNAL_SERVER_ERROR.getStatusCode());
+ Integer code = EXCEPTION_ERROR_CODES.get(ex.getClass());
+ if (code != null) {
+ return code;
+ }
+ if (ex instanceof WebApplicationException) {
+ return ((WebApplicationException) ex).getResponse().getStatus();
+ }
+ return Status.INTERNAL_SERVER_ERROR.getStatusCode();
}
/**
@@ -118,7 +131,7 @@ public class IcebergExceptionMapper implements
ExceptionMapper<Throwable> {
|| e instanceof ValidationException) {
return new BadRequestException("%s", message);
}
- if (EXCEPTION_ERROR_CODES.containsKey(e.getClass())) {
+ if (EXCEPTION_ERROR_CODES.containsKey(e.getClass()) || e instanceof
WebApplicationException) {
return e;
}
return new ServiceFailureException("%s", message);
@@ -138,8 +151,9 @@ public class IcebergExceptionMapper implements
ExceptionMapper<Throwable> {
public static Response toRESTResponse(Throwable ex) {
ServerHealth.getInstance().recordFailure(ex);
int status =
- EXCEPTION_ERROR_CODES.getOrDefault(
- ex.getClass(), Status.INTERNAL_SERVER_ERROR.getStatusCode());
+ ex instanceof Exception
+ ? getErrorCode((Exception) ex)
+ : Status.INTERNAL_SERVER_ERROR.getStatusCode();
if (status == Status.INTERNAL_SERVER_ERROR.getStatusCode()) {
LOG.error("Iceberg REST server unexpected failure:", ex);
} else {
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergExceptionMapper.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergExceptionMapper.java
index 4226dfdf5e..1ce5ebc6ba 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergExceptionMapper.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/TestIcebergExceptionMapper.java
@@ -18,7 +18,10 @@
*/
package org.apache.gravitino.iceberg.service;
+import javax.ws.rs.NotAcceptableException;
+import javax.ws.rs.NotAllowedException;
import javax.ws.rs.NotFoundException;
+import javax.ws.rs.NotSupportedException;
import javax.ws.rs.core.Response;
import org.apache.gravitino.exceptions.TokenExpiredException;
import org.apache.gravitino.server.web.ServerHealth;
@@ -57,6 +60,9 @@ public class TestIcebergExceptionMapper {
checkExceptionStatus(new NoSuchTableException(""), 404);
checkExceptionStatus(new NoSuchIcebergTableException(""), 404);
checkExceptionStatus(new UnsupportedOperationException(""), 406);
+ checkExceptionStatus(new NotAcceptableException(), 406);
+ checkExceptionStatus(new NotAllowedException("GET"), 405);
+ checkExceptionStatus(new NotSupportedException("unsupported content
type"), 415);
checkExceptionStatus(new AlreadyExistsException(""), 409);
checkExceptionStatus(new CommitFailedException(""), 409);
checkExceptionStatus(new UnprocessableEntityException(""), 422);
@@ -65,6 +71,24 @@ public class TestIcebergExceptionMapper {
checkExceptionStatus(new RuntimeException(), 500);
}
+ @Test
+ public void testWebApplicationExceptionPreservesStatusInErrorBody() {
+ try (Response response = icebergExceptionMapper.toResponse(new
NotAcceptableException())) {
+ Assertions.assertEquals(406, response.getStatus());
+ ErrorResponse entity = (ErrorResponse) response.getEntity();
+ Assertions.assertEquals(406, entity.code());
+ Assertions.assertEquals("NotAcceptableException", entity.type());
+ }
+ }
+
+ @Test
+ public void testConvertToIcebergExceptionKeepsWebApplicationException() {
+ NotAcceptableException original = new NotAcceptableException();
+ Exception converted =
IcebergExceptionMapper.convertToIcebergException(original);
+ Assertions.assertSame(original, converted);
+ Assertions.assertEquals(406,
IcebergExceptionMapper.getErrorCode(converted));
+ }
+
/** Checks that errors retain their type and nested causes in the response.
*/
@Test
public void testErrorsRetainTypeAndCause() {
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergConfig.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergConfig.java
index b9192d31db..67fa0a3b52 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergConfig.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergConfig.java
@@ -25,7 +25,9 @@ import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
+import org.apache.gravitino.iceberg.service.IcebergObjectMapperProvider;
import org.apache.iceberg.rest.responses.ConfigResponse;
+import org.apache.iceberg.rest.responses.ErrorResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -106,6 +108,23 @@ public class TestIcebergConfig extends IcebergTestBase {
Assertions.assertEquals(404, response.getStatus());
}
+ @Test
+ public void testConfigUnacceptableAcceptReturns406() {
+ // Gravitino media types are for /api/*, not Iceberg REST. Content
negotiation must return
+ // 406 (and the Iceberg error body code must match), not a misleading 500.
+ try (Response resp =
+ target(IcebergRestTestUtil.CONFIG_PATH)
+ .register(IcebergObjectMapperProvider.class)
+ .request()
+ .accept("application/vnd.gravitino.v1+json")
+ .get()) {
+ Assertions.assertEquals(Response.Status.NOT_ACCEPTABLE.getStatusCode(),
resp.getStatus());
+ ErrorResponse error = resp.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(406, error.code());
+ Assertions.assertEquals("NotAcceptableException", error.type());
+ }
+ }
+
@Test
public void testConfigEndpointsContainViewOperations() {
String warehouseName = IcebergRestTestUtil.PREFIX;
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
index dab90563fc..fb491d260a 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
@@ -198,8 +198,9 @@ public class TestIcebergNamespaceOperations extends
IcebergNamespaceTestBase {
Assertions.assertTrue(
dummyEventListener.popPostEvent() instanceof
IcebergDropNamespaceFailureEvent);
- // jersery route failed
- verifyDropNamespaceFail(500, Namespace.of(""));
+ // Empty namespace encodes to a path Jersey cannot bind for DELETE → 405
Method Not Allowed
+ // (previously mis-mapped to 500 by IcebergExceptionMapper).
+ verifyDropNamespaceFail(405, Namespace.of(""));
verifyCreateNamespaceSucc(Namespace.of("drop_foo3", "a"));
verifyDropNamespaceFail(404, Namespace.of("drop_foo3", "b"));