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 b0982cba52 [#14581] fix(lance-rest): validate registration and
preserve routing errors (#14583)
b0982cba52 is described below
commit b0982cba52e62c30fc3b72d15288f8e310c26216
Author: Qi Yu <[email protected]>
AuthorDate: Wed Nov 4 09:23:30 2026 +0800
[#14581] fix(lance-rest): validate registration and preserve routing errors
(#14583)
### What changes were proposed in this pull request?
Preserve Jersey 405/406/415 routing statuses in Lance REST with the
invalid-input error envelope and retain Allow for 405. Validate a
registration body and its location before constructing properties or
accessing the namespace backend.
### Why are the changes needed?
These invalid requests currently fall into internal-error handling and
return HTTP 500/code 18.
Fix: #14581
### Does this PR introduce _any_ user-facing change?
Unsupported methods/media types return their routing statuses. Missing
bodies/locations return HTTP 400/code 13. Valid registrations and
existing authentication/error-detail behavior are preserved. No protocol
dependency upgrade or storage change.
### How was this patch tested?
- All 12 TestLanceExceptionMapper and TestRegisterTableValidation tests
passed.
- Real Jersey routing checks cover 405 with Allow, 406, and 415.
- Registration tests verify invalid requests do not call the backend and
valid locations do reach it.
- Existing internal-error redaction and authentication response tests
pass.
- Reverting the two production files reproduces the new regression
failures.
- Module Spotless checks and git diff --check passed.
- No external Lance/Spark backend integration tests were run.
---
.../lance/service/LanceExceptionMapper.java | 23 ++++++
.../lance/service/rest/LanceTableOperations.java | 6 +-
.../lance/service/TestLanceExceptionMapper.java | 42 +++++++++++
.../service/rest/TestRegisterTableValidation.java | 86 ++++++++++++++++++++++
4 files changed, 154 insertions(+), 3 deletions(-)
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
index daad886540..648abbe402 100644
---
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
@@ -20,6 +20,11 @@ package org.apache.gravitino.lance.service;
import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
+import javax.ws.rs.NotAcceptableException;
+import javax.ws.rs.NotAllowedException;
+import javax.ws.rs.NotSupportedException;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@@ -77,6 +82,24 @@ public class LanceExceptionMapper implements
ExceptionMapper<Throwable> {
public static Response toRESTResponse(String instance, Throwable ex) {
ServerHealth.getInstance().recordFailure(ex);
+ if (ex instanceof NotAllowedException
+ || ex instanceof NotAcceptableException
+ || ex instanceof NotSupportedException) {
+ WebApplicationException routingError = (WebApplicationException) ex;
+ InvalidInputException invalidInput =
+ new InvalidInputException(ex.getMessage(), errorDetail(ex),
instance);
+ try (Response mapped = handleLanceNamespaceException(invalidInput)) {
+ Response.ResponseBuilder response =
+ Response.status(routingError.getResponse().getStatus())
+ .entity(mapped.getEntity())
+ .type(MediaType.APPLICATION_JSON_TYPE);
+ if (ex instanceof NotAllowedException) {
+ response.allow(routingError.getResponse().getAllowedMethods());
+ }
+ return response.build();
+ }
+ }
+
LanceNamespaceException lanceException =
ex instanceof LanceNamespaceException
? (LanceNamespaceException) ex
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
index 5eb2f731ed..fb763de1fb 100644
---
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
@@ -333,9 +333,9 @@ public class LanceTableOperations {
// No specific fields to validate for now
}
- private void validateRegisterTableRequest(
- @SuppressWarnings("unused") RegisterTableRequest request) {
- // No specific fields to validate for now
+ private void validateRegisterTableRequest(RegisterTableRequest request) {
+ Preconditions.checkArgument(request != null, "Request body must not be
null");
+ Preconditions.checkArgument(request.getLocation() != null, "location must
not be null");
}
private void validateDeregisterTableRequest(
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
index 42c4faac2f..2074225e6e 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
@@ -19,8 +19,12 @@
package org.apache.gravitino.lance.service;
import java.io.IOException;
+import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
+import javax.ws.rs.POST;
import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@@ -49,11 +53,25 @@ public class TestLanceExceptionMapper extends JerseyTest {
* @return never returns normally
*/
@GET
+ @Produces(MediaType.APPLICATION_JSON)
public String fail() {
AssertionError error = new AssertionError("assertion failure");
error.initCause(new IllegalStateException("root cause"));
throw error;
}
+
+ /**
+ * Accepts JSON to exercise unsupported request media types.
+ *
+ * @param body the request body
+ * @return the accepted body
+ */
+ @POST
+ @Path("consume")
+ @Consumes(MediaType.APPLICATION_JSON)
+ public String consume(String body) {
+ return body;
+ }
}
/**
@@ -75,6 +93,30 @@ public class TestLanceExceptionMapper extends JerseyTest {
.register(JacksonFeature.class);
}
+ @Test
+ void testMethodNotAllowed() {
+ try (Response response = target("error").request().put(Entity.json("{}")))
{
+ Assertions.assertEquals(405, response.getStatus());
+ Assertions.assertTrue(response.getAllowedMethods().contains("GET"));
+ Assertions.assertEquals(13,
response.readEntity(ErrorResponse.class).getCode());
+ }
+ }
+
+ @Test
+ void testUnsupportedMediaType() {
+ try (Response response =
target("error/consume").request().post(Entity.text("body"))) {
+ Assertions.assertEquals(415, response.getStatus());
+ Assertions.assertEquals(13,
response.readEntity(ErrorResponse.class).getCode());
+ }
+ }
+
+ @Test
+ void testNotAcceptable() {
+ try (Response response =
target("error").request(MediaType.TEXT_PLAIN).get()) {
+ Assertions.assertEquals(406, response.getStatus());
+ }
+ }
+
/** Verifies that an uncaught error is converted to a Lance internal error
response. */
@Test
public void testErrorResponse() {
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestRegisterTableValidation.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestRegisterTableValidation.java
new file mode 100644
index 0000000000..2db9c699f2
--- /dev/null
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestRegisterTableValidation.java
@@ -0,0 +1,86 @@
+/*
+ * 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.lance.service.rest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.lance.common.ops.NamespaceWrapper;
+import org.junit.jupiter.api.Test;
+import org.lance.namespace.model.ErrorResponse;
+import org.lance.namespace.model.RegisterTableRequest;
+import org.lance.namespace.model.RegisterTableResponse;
+
+class TestRegisterTableValidation {
+ @Test
+ void testValidLocationReachesBackend() {
+ NamespaceWrapper backend = mock(NamespaceWrapper.class,
RETURNS_DEEP_STUBS);
+ RegisterTableRequest request = new RegisterTableRequest();
+ request.setLocation("s3://bucket/existing.lance");
+ when(backend
+ .asTableOps()
+ .registerTable(
+ eq("cat$schema$table"),
+ eq("create"),
+ eq("$"),
+ argThat(
+ properties ->
"s3://bucket/existing.lance".equals(properties.get("location")))))
+ .thenReturn(new RegisterTableResponse());
+ try (Response response =
+ new LanceTableOperations(backend).registerTable("cat$schema$table",
"$", null, request)) {
+ assertEquals(200, response.getStatus());
+ verify(backend.asTableOps())
+ .registerTable(
+ eq("cat$schema$table"),
+ eq("create"),
+ eq("$"),
+ argThat(
+ properties ->
"s3://bucket/existing.lance".equals(properties.get("location"))));
+ }
+ }
+
+ @Test
+ void testMissingLocation() {
+ NamespaceWrapper backend = mock(NamespaceWrapper.class);
+ LanceTableOperations resource = new LanceTableOperations(backend);
+ try (Response response =
+ resource.registerTable("cat$schema$table", "$", null, new
RegisterTableRequest())) {
+ assertEquals(400, response.getStatus());
+ assertEquals(13, ((ErrorResponse) response.getEntity()).getCode());
+ verifyNoInteractions(backend);
+ }
+ }
+
+ @Test
+ void testMissingBody() {
+ NamespaceWrapper backend = mock(NamespaceWrapper.class);
+ try (Response response =
+ new LanceTableOperations(backend).registerTable("cat$schema$table",
"$", null, null)) {
+ assertEquals(400, response.getStatus());
+ verifyNoInteractions(backend);
+ }
+ }
+}