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 4207d3fca8 [#11553] feat(lance): add health check endpoints for Lance 
REST servers (#11558)
4207d3fca8 is described below

commit 4207d3fca8d09ef9ee85ed1dd49121a3b864d753
Author: tian bao <[email protected]>
AuthorDate: Tue Jun 30 17:52:18 2026 +0800

    [#11553] feat(lance): add health check endpoints for Lance REST servers 
(#11558)
    
    ### What changes were proposed in this pull request?
    
    This PR adds health check endpoints and a custom authentication filter
    for the Lance REST server, aligning it with the Iceberg REST server
    implementation.
    
    1、LanceAuthenticationFilter — A new subclass of AuthenticationFilter
    that whitelists /lance/health and /lance/health/* paths to bypass
    authentication, in addition to the default health paths (/health,
    /api/health). This allows Kubernetes probes and monitoring systems to
    reach the Lance health endpoints without credentials.
    
    2、LanceHealthOperations — A new JAX-RS resource at /health/live that
    returns a MicroProfile Health-compatible liveness response (200 OK with
    UP status).
    
    3、LanceRESTService — Updated to inject LanceAuthenticationFilter into
    the Jetty server via createAuthenticationFilter() override.
    
    4、build.gradle.kts — Added testImplementation(libs.junit.jupiter.params)
    dependency for @ParameterizedTest support.
    
    ### Why are the changes needed?
    
    - Health check endpoints are required for Kubernetes liveness probes and
    monitoring integration.
    - Without LanceAuthenticationFilter, all Lance REST endpoints (including
    health checks) require authentication, making health probes fail.
    
    #11553
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. The Lance REST server now exposes:
    
    - GET /lance/health/live — Liveness probe endpoint (no authentication
    required).
    
    ### How was this patch tested?
    
    Added unit tests:
    
    - TestLanceAuthenticationFilter — Verifies health paths bypass auth and
    non-health paths require auth.
    - TestLanceHealthOperations — Verifies the liveness endpoint returns 200
    with UP status.
---
 .../lance/common/ops/NamespaceWrapper.java         |   4 +
 lance/lance-rest-server/build.gradle.kts           |   1 +
 .../apache/gravitino/lance/LanceJettyServer.java   |  35 +++++
 .../apache/gravitino/lance/LanceRESTService.java   |  13 +-
 .../lance/service/LanceAuthenticationFilter.java   |  86 +++++++++++
 .../lance/service/LanceHealthCheckPathMatcher.java |  41 ++++++
 .../lance/service/rest/LanceHealthOperations.java  | 142 +++++++++++++++++++
 .../service/TestLanceAuthenticationFilter.java     | 157 +++++++++++++++++++++
 .../service/rest/TestLanceHealthOperations.java    |  96 +++++++++++++
 9 files changed, 573 insertions(+), 2 deletions(-)

diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
index 936a5a7069..5973fa4f52 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
@@ -53,6 +53,10 @@ public abstract class NamespaceWrapper {
     return tableOps;
   }
 
+  public boolean isInitialized() {
+    return initialized;
+  }
+
   public LanceConfig config() {
     return config;
   }
diff --git a/lance/lance-rest-server/build.gradle.kts 
b/lance/lance-rest-server/build.gradle.kts
index 127eac64d5..3235b0dc17 100644
--- a/lance/lance-rest-server/build.gradle.kts
+++ b/lance/lance-rest-server/build.gradle.kts
@@ -133,6 +133,7 @@ dependencies {
   }
 
   testImplementation(libs.junit.jupiter.api)
+  testImplementation(libs.junit.jupiter.params)
   testImplementation(libs.mockito.inline)
   testImplementation(libs.mysql.driver)
   testImplementation(libs.postgresql.driver)
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceJettyServer.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceJettyServer.java
new file mode 100644
index 0000000000..be8b8dbff5
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceJettyServer.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import javax.servlet.Filter;
+import org.apache.gravitino.lance.service.LanceAuthenticationFilter;
+import org.apache.gravitino.server.web.JettyServer;
+
+/**
+ * A {@link JettyServer} subclass that creates a {@link 
LanceAuthenticationFilter} for the Lance
+ * REST service authentication layer.
+ */
+class LanceJettyServer extends JettyServer {
+
+  @Override
+  protected Filter createAuthenticationFilter() {
+    return new LanceAuthenticationFilter();
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
index 407350c000..3375dd9060 100644
--- 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
@@ -28,10 +28,12 @@ import 
org.apache.gravitino.auxiliary.GravitinoAuxiliaryService;
 import org.apache.gravitino.lance.common.config.LanceConfig;
 import org.apache.gravitino.lance.common.ops.LanceNamespaceBackend;
 import org.apache.gravitino.lance.common.ops.NamespaceWrapper;
+import org.apache.gravitino.lance.service.LanceHealthCheckPathMatcher;
 import org.apache.gravitino.listener.EventBus;
 import org.apache.gravitino.listener.api.event.EventSource;
 import org.apache.gravitino.metrics.MetricsSystem;
 import org.apache.gravitino.metrics.source.MetricsSource;
+import org.apache.gravitino.server.web.HealthAliasServlet;
 import org.apache.gravitino.server.web.HttpAuditFilter;
 import org.apache.gravitino.server.web.HttpServerMetricsSource;
 import org.apache.gravitino.server.web.JettyServer;
@@ -66,7 +68,7 @@ public class LanceRESTService implements 
GravitinoAuxiliaryService {
     LanceConfig lanceConfig = new LanceConfig(properties);
     JettyServerConfig serverConfig = JettyServerConfig.fromConfig(lanceConfig);
 
-    server = new JettyServer();
+    server = new LanceJettyServer();
     // Get MetricsSystem and EventBus from GravitinoEnv once at init time.
     MetricsSystem metricsSystem = GravitinoEnv.getInstance().metricsSystem();
     EventBus eventBus = GravitinoEnv.getInstance().eventBus();
@@ -94,10 +96,17 @@ public class LanceRESTService implements 
GravitinoAuxiliaryService {
     Servlet container = new ServletContainer(resourceConfig);
     server.addServlet(container, LANCE_SPEC);
     server.addFilter(
-        new HttpAuditFilter(eventBus, 
EventSource.GRAVITINO_LANCE_REST_SERVER), LANCE_SPEC);
+        new HttpAuditFilter(
+            eventBus, EventSource.GRAVITINO_LANCE_REST_SERVER, new 
LanceHealthCheckPathMatcher()),
+        LANCE_SPEC);
     server.addCustomFilters(LANCE_SPEC);
     server.addSystemFilters(LANCE_SPEC);
 
+    // Root-level aliases for health checks to improve compatibility with 
various monitoring
+    // systems that expect a /health endpoint.
+    server.addServlet(new HealthAliasServlet("/lance"), "/health/*");
+    server.addServlet(new HealthAliasServlet("/lance"), "/health.html");
+
     LOG.info(
         "Initialized Lance REST service for backend {} in {} mode",
         lanceConfig.getNamespaceBackend(),
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceAuthenticationFilter.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceAuthenticationFilter.java
new file mode 100644
index 0000000000..51aea85524
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceAuthenticationFilter.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;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
+import org.apache.gravitino.server.authentication.AuthenticationFilter;
+import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.lance.namespace.model.ErrorResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An {@link AuthenticationFilter} subclass for the Lance REST server that:
+ *
+ * <ul>
+ *   <li>allows health check endpoints to bypass authentication via {@link
+ *       LanceHealthCheckPathMatcher};
+ *   <li>returns Lance-compatible JSON error responses on authentication 
failure instead of the
+ *       default HTML error pages.
+ * </ul>
+ */
+public class LanceAuthenticationFilter extends AuthenticationFilter {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(LanceAuthenticationFilter.class);
+  private static final ObjectMapper MAPPER = 
ObjectMapperProvider.objectMapper();
+
+  public LanceAuthenticationFilter() {
+    healthCheckMatcher = new LanceHealthCheckPathMatcher();
+  }
+
+  @Override
+  protected void sendAuthErrorResponse(HttpServletResponse response, Exception 
exception)
+      throws IOException {
+    int status;
+    String message;
+    if (exception instanceof UnauthorizedException) {
+      status = HttpServletResponse.SC_UNAUTHORIZED;
+      message = exception.getMessage();
+      if (message == null || message.isEmpty()) {
+        message = "Authentication failed";
+      }
+    } else if (exception instanceof ForbiddenException) {
+      status = HttpServletResponse.SC_FORBIDDEN;
+      message = exception.getMessage();
+      if (message == null || message.isEmpty()) {
+        message = "Access denied";
+      }
+    } else {
+      status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+      LOG.error("Authentication failure", exception);
+      message = "Authentication failed";
+    }
+
+    ErrorResponse errorResponse = new ErrorResponse();
+    errorResponse.setCode(status);
+    errorResponse.setError(message);
+    errorResponse.setDetail("");
+    errorResponse.setInstance("");
+
+    response.setStatus(status);
+    response.setContentType("application/json");
+    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+    MAPPER.writeValue(response.getWriter(), errorResponse);
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceHealthCheckPathMatcher.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceHealthCheckPathMatcher.java
new file mode 100644
index 0000000000..e18745fce6
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceHealthCheckPathMatcher.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+import org.apache.gravitino.server.web.HealthCheckPathMatcher;
+
+/**
+ * A {@link HealthCheckPathMatcher} for the Lance REST server that 
additionally recognises {@code
+ * /lance/health} and {@code /lance/health/*} as health check endpoints.
+ *
+ * <p>Pass an instance of this class to both {@link
+ * org.apache.gravitino.server.authentication.AuthenticationFilter} (via {@code
+ * LanceAuthenticationFilter}) and {@link 
org.apache.gravitino.server.web.HttpAuditFilter} when
+ * constructing the Lance REST server so that both filters agree on which 
paths are probe traffic.
+ */
+public class LanceHealthCheckPathMatcher extends HealthCheckPathMatcher {
+
+  @Override
+  public boolean isHealthCheckPath(String path) {
+    if (super.isHealthCheckPath(path)) {
+      return true;
+    }
+    return path.equals("/lance/health") || path.startsWith("/lance/health/");
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceHealthOperations.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceHealthOperations.java
new file mode 100644
index 0000000000..4c280d9f8b
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceHealthOperations.java
@@ -0,0 +1,142 @@
+/*
+ * 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 com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.HealthCheckDTO;
+import org.apache.gravitino.dto.responses.HealthResponse;
+import org.apache.gravitino.lance.common.ops.NamespaceWrapper;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.web.Utils;
+
+/**
+ * Health check endpoints for the Lance REST server. Follows the same 
MicroProfile Health semantics
+ * as the main Gravitino server.
+ *
+ * <ul>
+ *   <li>{@code GET /lance/health/live} — liveness, 200 as long as the HTTP 
thread can respond
+ *   <li>{@code GET /lance/health/ready} — readiness, 200 when the namespace 
wrapper is initialized
+ *   <li>{@code GET /lance/health} — aggregate, 200 when both pass
+ * </ul>
+ *
+ * All endpoints return 503 with a JSON body describing the failed check(s) 
when unhealthy.
+ */
+@Path("/health")
+@Produces(MediaType.APPLICATION_JSON)
+public class LanceHealthOperations {
+
+  private static final String CHECK_HTTP_SERVER = "httpServer";
+  private static final String CHECK_NAMESPACE_WRAPPER = "namespaceWrapper";
+
+  @Inject private NamespaceWrapper namespaceWrapper;
+
+  /** Default constructor for Jersey auto-discovery. */
+  public LanceHealthOperations() {}
+
+  /**
+   * Liveness probe. Returns 200 as long as the HTTP thread can respond.
+   *
+   * @return 200 OK with an UP {@link HealthResponse}
+   */
+  @GET
+  @Path("/live")
+  @Timed(name = "lance.health.live." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "lance.health.live", absolute = true)
+  public Response live() {
+    HealthCheckDTO check = up(CHECK_HTTP_SERVER, Collections.emptyMap());
+    HealthResponse healthResponse =
+        new HealthResponse(HealthCheckDTO.Status.UP, 
Collections.singletonList(check));
+    return Utils.ok(healthResponse);
+  }
+
+  /**
+   * Readiness probe. Returns 200 when the {@link NamespaceWrapper} is 
initialized, 503 otherwise.
+   *
+   * @return 200 OK when ready, 503 Service Unavailable with a DOWN {@link 
HealthResponse} otherwise
+   */
+  @GET
+  @Path("/ready")
+  @Timed(name = "lance.health.ready." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "lance.health.ready", absolute = true)
+  public Response ready() {
+    HealthCheckDTO namespaceCheck = checkNamespaceWrapper();
+    HealthCheckDTO.Status overall = namespaceCheck.getStatus();
+    HealthResponse body = new HealthResponse(overall, 
Collections.singletonList(namespaceCheck));
+    return overall == HealthCheckDTO.Status.UP ? Utils.ok(body) : 
Utils.serviceUnavailable(body);
+  }
+
+  /**
+   * Aggregate health check. Returns 200 when both liveness and readiness 
pass, 503 otherwise.
+   *
+   * @return 200 OK when healthy, 503 Service Unavailable with failing checks 
described in the body
+   */
+  @GET
+  @Timed(name = "lance.health." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
+  @ResponseMetered(name = "lance.health", absolute = true)
+  public Response health() {
+    List<HealthCheckDTO> checks = new ArrayList<>(2);
+    checks.add(up(CHECK_HTTP_SERVER, Collections.emptyMap()));
+    checks.add(checkNamespaceWrapper());
+
+    HealthCheckDTO.Status overall =
+        checks.stream().anyMatch(c -> c.getStatus() == 
HealthCheckDTO.Status.DOWN)
+            ? HealthCheckDTO.Status.DOWN
+            : HealthCheckDTO.Status.UP;
+
+    HealthResponse body = new HealthResponse(overall, checks);
+    return overall == HealthCheckDTO.Status.UP ? Utils.ok(body) : 
Utils.serviceUnavailable(body);
+  }
+
+  private HealthCheckDTO checkNamespaceWrapper() {
+    NamespaceWrapper wrapper = getNamespaceWrapper();
+    if (wrapper == null) {
+      return down(CHECK_NAMESPACE_WRAPPER, "reason", "namespace wrapper not 
initialized");
+    }
+    if (wrapper.isInitialized()) {
+      return up(CHECK_NAMESPACE_WRAPPER, Collections.emptyMap());
+    } else {
+      return down(CHECK_NAMESPACE_WRAPPER, "reason", "namespace wrapper not 
initialized");
+    }
+  }
+
+  /** Visible for testing — subclasses override to inject a different wrapper 
instance. */
+  NamespaceWrapper getNamespaceWrapper() {
+    return namespaceWrapper;
+  }
+
+  private static HealthCheckDTO up(String name, Map<String, String> details) {
+    return new HealthCheckDTO(name, HealthCheckDTO.Status.UP, details);
+  }
+
+  private static HealthCheckDTO down(String name, String detailKey, String 
detailValue) {
+    return new HealthCheckDTO(
+        name, HealthCheckDTO.Status.DOWN, Collections.singletonMap(detailKey, 
detailValue));
+  }
+}
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceAuthenticationFilter.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceAuthenticationFilter.java
new file mode 100644
index 0000000000..0b73c7ae7f
--- /dev/null
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceAuthenticationFilter.java
@@ -0,0 +1,157 @@
+/*
+ * 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;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
+import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.lance.namespace.model.ErrorResponse;
+
+public class TestLanceAuthenticationFilter {
+
+  private static final ObjectMapper MAPPER = 
ObjectMapperProvider.objectMapper();
+
+  /** Exposes the protected {@code isHealthCheckRequest} method for white-box 
testing. */
+  private static class TestableFilter extends LanceAuthenticationFilter {
+    boolean isHealth(ServletRequest request) {
+      return isHealthCheckRequest(request);
+    }
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "/lance/health",
+        "/lance/health/live",
+        "/lance/health/ready",
+        "/health",
+        "/health/live",
+        "/health/ready",
+        "/health.html",
+        "/api/health",
+        "/api/health/live",
+        "/api/health/ready"
+      })
+  public void testHealthPathsBypassAuth(String path) {
+    TestableFilter filter = new TestableFilter();
+    HttpServletRequest req = mock(HttpServletRequest.class);
+    when(req.getRequestURI()).thenReturn(path);
+    Assertions.assertTrue(filter.isHealth(req), "Expected health bypass for 
path: " + path);
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "/lance/v1/namespace",
+        "/lance/v1/table",
+        "/lance/healthcheck",
+        "/iceberg/health",
+        "/iceberg/v1/namespaces"
+      })
+  public void testNonHealthPathsRequireAuth(String path) {
+    TestableFilter filter = new TestableFilter();
+    HttpServletRequest req = mock(HttpServletRequest.class);
+    when(req.getRequestURI()).thenReturn(path);
+    Assertions.assertFalse(filter.isHealth(req), "Expected auth required for 
path: " + path);
+  }
+
+  @Test
+  public void testNonHttpRequestReturnsFalse() {
+    TestableFilter filter = new TestableFilter();
+    ServletRequest nonHttpRequest = mock(ServletRequest.class);
+    Assertions.assertFalse(filter.isHealth(nonHttpRequest));
+  }
+
+  @Test
+  public void testUnauthorizedErrorReturnsJson() throws Exception {
+    LanceAuthenticationFilter filter = new LanceAuthenticationFilter();
+
+    HttpServletResponse response = mock(HttpServletResponse.class);
+    StringWriter stringWriter = new StringWriter();
+    PrintWriter printWriter = new PrintWriter(stringWriter);
+    when(response.getWriter()).thenReturn(printWriter);
+
+    filter.sendAuthErrorResponse(
+        response, new UnauthorizedException("The provided credentials did not 
support"));
+
+    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    verify(response).setContentType("application/json");
+    verify(response).setCharacterEncoding("UTF-8");
+
+    printWriter.flush();
+    String json = stringWriter.toString();
+    ErrorResponse errorResponse = MAPPER.readValue(json, ErrorResponse.class);
+    Assertions.assertEquals(401, errorResponse.getCode());
+    Assertions.assertEquals("The provided credentials did not support", 
errorResponse.getError());
+  }
+
+  @Test
+  public void testForbiddenExceptionReturnsJson() throws Exception {
+    LanceAuthenticationFilter filter = new LanceAuthenticationFilter();
+
+    HttpServletResponse response = mock(HttpServletResponse.class);
+    StringWriter stringWriter = new StringWriter();
+    PrintWriter printWriter = new PrintWriter(stringWriter);
+    when(response.getWriter()).thenReturn(printWriter);
+
+    filter.sendAuthErrorResponse(response, new ForbiddenException("Access 
denied"));
+
+    verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN);
+
+    printWriter.flush();
+    String json = stringWriter.toString();
+    ErrorResponse errorResponse = MAPPER.readValue(json, ErrorResponse.class);
+    Assertions.assertEquals(403, errorResponse.getCode());
+    Assertions.assertEquals("Access denied", errorResponse.getError());
+  }
+
+  @Test
+  public void testInternalServerErrorReturnsJson() throws Exception {
+    LanceAuthenticationFilter filter = new LanceAuthenticationFilter();
+
+    HttpServletResponse response = mock(HttpServletResponse.class);
+    StringWriter stringWriter = new StringWriter();
+    PrintWriter printWriter = new PrintWriter(stringWriter);
+    when(response.getWriter()).thenReturn(printWriter);
+
+    filter.sendAuthErrorResponse(response, new RuntimeException("Something 
went wrong"));
+
+    verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+
+    printWriter.flush();
+    String json = stringWriter.toString();
+    ErrorResponse errorResponse = MAPPER.readValue(json, ErrorResponse.class);
+    Assertions.assertEquals(500, errorResponse.getCode());
+    Assertions.assertEquals("Authentication failed", errorResponse.getError());
+  }
+}
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceHealthOperations.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceHealthOperations.java
new file mode 100644
index 0000000000..24256fd67d
--- /dev/null
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceHealthOperations.java
@@ -0,0 +1,96 @@
+/*
+ * 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.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.HealthCheckDTO;
+import org.apache.gravitino.dto.responses.HealthResponse;
+import org.apache.gravitino.lance.common.ops.NamespaceWrapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestLanceHealthOperations {
+
+  private static LanceHealthOperations operationsWithWrapper(NamespaceWrapper 
wrapper) {
+    return new LanceHealthOperations() {
+      @Override
+      NamespaceWrapper getNamespaceWrapper() {
+        return wrapper;
+      }
+    };
+  }
+
+  @Test
+  public void testLiveReturns200() {
+    LanceHealthOperations ops = operationsWithWrapper(null);
+    Response resp = ops.live();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.UP, body.getStatus());
+  }
+
+  @Test
+  public void testReadyReturns200WhenWrapperInitialized() {
+    NamespaceWrapper wrapper = mock(NamespaceWrapper.class);
+    when(wrapper.isInitialized()).thenReturn(true);
+    LanceHealthOperations ops = operationsWithWrapper(wrapper);
+    Response resp = ops.ready();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.UP, body.getStatus());
+  }
+
+  @Test
+  public void testReadyReturns503WhenWrapperNotInitialized() {
+    LanceHealthOperations ops = operationsWithWrapper(null);
+    Response resp = ops.ready();
+    
Assertions.assertEquals(Response.Status.SERVICE_UNAVAILABLE.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.DOWN, body.getStatus());
+    Assertions.assertFalse(body.getChecks().isEmpty());
+    Assertions.assertEquals("namespaceWrapper", 
body.getChecks().get(0).getName());
+  }
+
+  @Test
+  public void testHealthReturns200WhenWrapperInitialized() {
+    NamespaceWrapper wrapper = mock(NamespaceWrapper.class);
+    when(wrapper.isInitialized()).thenReturn(true);
+    LanceHealthOperations ops = operationsWithWrapper(wrapper);
+    Response resp = ops.health();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.UP, body.getStatus());
+    Assertions.assertEquals(2, body.getChecks().size());
+  }
+
+  @Test
+  public void testHealthReturns503WhenWrapperNotInitialized() {
+    LanceHealthOperations ops = operationsWithWrapper(null);
+    Response resp = ops.health();
+    
Assertions.assertEquals(Response.Status.SERVICE_UNAVAILABLE.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.DOWN, body.getStatus());
+    boolean hasNamespaceCheck =
+        body.getChecks().stream().anyMatch(c -> 
"namespaceWrapper".equals(c.getName()));
+    Assertions.assertTrue(hasNamespaceCheck);
+  }
+}

Reply via email to