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 0d195cf4f5 [#12574] improvement(lance): document REST auth and add ITs 
(#12954)
0d195cf4f5 is described below

commit 0d195cf4f5173335f15ce1adf61306697b8c74cc
Author: Qi Yu <[email protected]>
AuthorDate: Mon Sep 14 11:28:00 2026 +0800

    [#12574] improvement(lance): document REST auth and add ITs (#12954)
    
    ### What changes were proposed in this pull request?
    
    Document Lance REST authentication, authorization, active roles, and
    deployment differences, with examples and regression coverage.
    
    Preserve backend authentication/authorization errors as HTTP 401/403,
    remove stack traces from those errors and auxiliary authorization
    denials, and return a generic response for unexpected HTTP 500 failures
    while retaining server-side logging.
    
    ### Why are the changes needed?
    
    Users need clear authentication guidance and correct, sanitized error
    responses. Backend access denials currently become HTTP 500.
    
    Fix: #12574
    
    The independent Arrow CreateTable fix is tracked by #12988 and
    implemented in #12989. Authentication-related fixes stay here because
    the authentication work has not been merged into branch-1.3.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Improved authentication documentation; backend
    authentication/authorization errors retain HTTP 401/403; authorization
    denials omit stack traces; unexpected HTTP 500 responses omit internal
    exception details. No identity propagation or configuration changes.
    
    ### How was this patch tested?
    
    86 tests passed: 75 service unit tests and 11 integration tests across
    LanceNamespaceAuthorizationIT and LanceRESTServiceAuthIT. No failures or
    skips.
    
    Coverage includes error mapping and sanitization, auxiliary
    authentication and active roles, and standalone backend access denial
    through a separate production server JVM. Relevant Spotless formatting
    passed. Tests used the embedded backend; Docker and the full deployment
    matrix were not run.
---
 docs/lance-rest-integration.md                     |  52 ++++-
 docs/lance-rest-service.md                         | 172 ++++++++++++--
 lance/lance-rest-server/build.gradle.kts           |   1 +
 .../lance/service/LanceExceptionMapper.java        |  12 +-
 ...anceMetadataAuthorizationMethodInterceptor.java |   6 +-
 .../LanceRESTAuthInterceptionService.java          |   3 +-
 .../test/LanceNamespaceAuthorizationIT.java        | 258 +++++++++++++++++++++
 .../lance/service/TestLanceExceptionMapper.java    |  58 ++++-
 .../service/rest/TestLanceNamespaceOperations.java |  29 ++-
 9 files changed, 547 insertions(+), 44 deletions(-)

diff --git a/docs/lance-rest-integration.md b/docs/lance-rest-integration.md
index 6942b01df4..18f581f522 100644
--- a/docs/lance-rest-integration.md
+++ b/docs/lance-rest-integration.md
@@ -21,7 +21,7 @@ This documentation assumes familiarity with the Lance REST 
service setup as desc
 The following table outlines the tested compatibility between Gravitino 
versions and Lance connector versions:
 
 | Gravitino Version (Lance REST) | Supported lance-spark Versions | Supported 
lance-ray Versions                  |
-|--------------------------------|--------------------------------|-----------------------------------------------|
+| ------------------------------ | ------------------------------ | 
--------------------------------------------- |
 | 1.1.1 - 1.2.1                  | 0.0.10 - 0.0.15                | 0.0.6 - 
0.0.8                                 |
 | 1.3.0                          | 0.2.0, 0.4.0, 0.5.1            | 0.3.0 - 
0.4.2 (0.2.0 conditionally supported) |
 
@@ -109,6 +109,56 @@ Before proceeding, ensure the following requirements are 
met:
     - For Spark integration: `pyspark`
     - For Ray integration: `ray`, `lance-namespace`, `lance-ray`
 
+## Authentication and authorization
+
+For per-user metadata authorization, connect engines to the auxiliary Lance 
REST service with
+`gravitino.authorization.enable=true`. Configure each engine's REST client to 
send the caller's
+`Authorization` header on every namespace and table request. If supported by 
that client version,
+`X-Gravitino-Active-Roles` can restrict the active roles. See the
+[Lance REST authentication and privilege 
matrix](./lance-rest-service.md#authentication-and-authorization).
+
+For example, with development-only `simple` authentication, this request lists 
only tables that
+`user1` may access (the password is not validated):
+
+```shell
+curl --user 'user1:unused' \
+  -H 'X-Gravitino-Active-Roles: ALL' \
+  
'http://localhost:9101/lance/v1/namespace/lance_catalog.sales/table/list?delimiter=.'
+```
+
+Connector header configuration depends on the connector version. The Spark and 
Ray examples
+below omit credentials and assume the default simple-authentication setup; in 
auxiliary mode
+such requests use the configured Lance service identity. They do not 
demonstrate per-user
+access control. In standalone mode, all metadata requests to Gravitino use the 
backend service
+identity even when an engine supplies its own incoming credentials.
+
+Engines that probe before creating need the corresponding creation privileges. 
Reading table
+metadata requires `SELECT_TABLE` or `MODIFY_TABLE` with parent access, while 
overwriting requires
+`MODIFY_TABLE` and dropping requires ownership. Metadata authorization does 
not authorize direct
+reads or writes to object storage: configure storage access independently. 
Lance REST responses
+can return shared storage credentials configured on the catalog or table; it 
does not issue
+per-user, scoped storage credentials.
+
+### Verify authentication and authorization locally
+
+The HTTP integration suites start Gravitino with the Lance auxiliary service 
and exercise
+caller identity, service identity fallback, active roles, namespace and table 
privileges,
+filtered listings, denied mutations, and rejection of non-empty Arrow creates 
without side effects.
+They also start standalone Lance REST through its production entry point in a 
separate JVM to verify
+its outbound service identity and propagation of backend authorization denials 
through the Gravitino
+HTTP API.
+
+```shell
+./gradlew :lance:lance-rest-server:test \
+  --tests '*LanceRESTServiceAuthIT' \
+  --tests '*LanceNamespaceAuthorizationIT' \
+  --tests '*LanceTableAuthorizationIT' \
+  -PskipDockerTests=true
+```
+
+These suites use `simple` authentication and local storage. They do not 
validate an external
+OAuth2/Kerberos provider or object-store access policies.
+
 ## Spark Integration
 
 ### Configuration
diff --git a/docs/lance-rest-service.md b/docs/lance-rest-service.md
index 7186243bb2..14b9c61a01 100644
--- a/docs/lance-rest-service.md
+++ b/docs/lance-rest-service.md
@@ -57,7 +57,7 @@ The Lance REST service acts as a bridge between Lance 
datasets and applications:
 ```
 
 **Key Features:**
-- Full compliance with Lance REST API specification
+- Support for the Lance REST operations listed below
 - Can run standalone or integrated with Gravitino server
 - Support for namespace and table management
 - Metadata stored in Gravitino for unified governance
@@ -67,7 +67,7 @@ The Lance REST service acts as a bridge between Lance 
datasets and applications:
 The Lance REST service provides comprehensive support for namespace management 
and table management. Index operations are not supported yet. The table below 
lists all supported operations:
 
 | Operation         | Description                                              
                 | HTTP Method | Endpoint Pattern                      |
-|-------------------|---------------------------------------------------------------------------|-------------|---------------------------------------|
+| ----------------- | 
------------------------------------------------------------------------- | 
----------- | ------------------------------------- |
 | CreateNamespace   | Create a new Lance namespace                             
                 | POST        | `/lance/v1/namespace/{id}/create`     |
 | ListNamespaces    | List all namespaces under a parent namespace             
                 | GET         | `/lance/v1/namespace/{parent}/list`   |
 | DescribeNamespace | Retrieve detailed information about a specific namespace 
                 | POST        | `/lance/v1/namespace/{id}/describe`   |
@@ -99,17 +99,24 @@ REST-style canonical form.
 - `overwrite`: Replaces existing namespace
 
 **DropNamespace** behavior:
-- Recursively deletes all child namespaces and tables
-- Deletes both metadata and Lance data files
-- Operation is irreversible
+- Defaults to `behavior=restrict`; non-empty namespaces cannot be dropped in 
this mode
+- `behavior=cascade` removes child metadata recursively
+- Cascading a schema drop uses table drop semantics: external Lance datasets 
are preserved
+- Use `DropTable` explicitly to delete a table's data; namespace deletion is 
not a storage purge
 
 #### Table Operations
 
 **RegisterTable vs CreateTable**:
 - **RegisterTable**: Links existing Lance datasets into Gravitino catalog 
without data movement
-- **CreateTable**: Creates new Lance table with schema and write metadata files
+- **CreateTable**: Creates an empty Lance dataset using the schema from the 
Arrow IPC stream
 :::note
-The `version` field of `CreateTable` response is always null, which stands for 
the latest version. 
+The current `CreateTable` implementation accepts schema-only Arrow streams, 
including zero-row
+batches. A stream containing rows returns HTTP `406` before any metadata or 
dataset changes,
+including for `overwrite`. Write records through a Lance client or engine 
after creation.
+
+The `version` field of `CreateTable` reports the stored Lance dataset version 
when available.
+`DescribeTable` currently returns the latest metadata even when a historical 
`version` is requested;
+versioned metadata reads are not implemented.
 :::
 
 **DropTable vs DeregisterTable**:
@@ -124,7 +131,7 @@ The `version` field of `CreateTable` response is always 
null, which stands for t
 To enable the Lance REST service within Gravitino server, configure the 
following properties in your Gravitino configuration file 
`${GRAVITINO_HOME}/conf/gravitino.conf`:
 
 | Configuration Property                    | Description                      
                                            | Default Value           | 
Required |
-|-------------------------------------------|------------------------------------------------------------------------------|-------------------------|----------|
+| ----------------------------------------- | 
---------------------------------------------------------------------------- | 
----------------------- | -------- |
 | `gravitino.auxService.names`              | Auxiliary services to run. 
Include `lance-rest` to enable Lance REST service | iceberg-rest,lance-rest | 
Yes      |
 | `gravitino.lance-rest.classpath`          | Classpath for Lance REST 
service, relative to Gravitino home directory       | lance-rest-server/libs  | 
Yes      |
 | `gravitino.lance-rest.httpPort`           | Port number for Lance REST 
service                                           | 9101                    | 
No       |
@@ -135,13 +142,13 @@ To enable the Lance REST service within Gravitino server, 
configure the followin
 
 **Authentication to the Gravitino Server**
 
-The Lance REST service makes its own requests to the Gravitino server. Those 
requests must carry
-credentials, otherwise a Gravitino server configured with an authenticator 
other than `simple`
-rejects them and every Lance operation fails. Configure the auth type to match 
the Gravitino
-server:
+In standalone mode, the Lance REST service makes HTTP requests to the 
Gravitino server using
+its configured service credentials. Configure the auth type to match the 
Gravitino server.
+Auxiliary mode uses internal APIs and preserves the authenticated caller 
instead; the simple
+user name below is only the fallback for requests accepted as anonymous.
 
 | Configuration Property                             | Description             
                                                           | Default Value      
 | Required          |
-|----------------------------------------------------|------------------------------------------------------------------------------------|---------------------|-------------------|
+| -------------------------------------------------- | 
----------------------------------------------------------------------------------
 | ------------------- | ----------------- |
 | `gravitino.lance-rest.gravitino-auth-type`         | Auth type used to reach 
the Gravitino server. Supported values: `simple`, `oauth2` | `simple`           
 | No                |
 | `gravitino.lance-rest.gravitino-simple.user-name`  | User name presented 
when the auth type is `simple`                                 | 
`lance-rest-server` | No                |
 | `gravitino.lance-rest.gravitino-oauth2.server-uri` | OAuth2 server URI       
                                                           | (none)             
 | Yes, for `oauth2` |
@@ -149,8 +156,8 @@ server:
 | `gravitino.lance-rest.gravitino-oauth2.token-path` | Path on the OAuth2 
server used to request the token                                | (none)        
      | Yes, for `oauth2` |
 | `gravitino.lance-rest.gravitino-oauth2.scope`      | Scope of the requested 
OAuth2 token                                                | (none)            
  | Yes, for `oauth2` |
 
-This setting controls how the service authenticates to the Gravitino server. 
It does not change how
-callers authenticate to the Lance REST service itself.
+These settings control outbound authentication in standalone mode. They do not 
configure inbound
+authentication to Lance REST. See [Authentication and 
authorization](#authentication-and-authorization).
 
 **Example Configuration:**
 
@@ -176,7 +183,7 @@ To run Lance REST service independently without Gravitino 
server (You need to st
 Configure the service by editing 
`{GRAVITINO_HOME}/conf/gravitino-lance-rest-server.conf` or passing 
command-line arguments:
 
 | Configuration Property                    | Description                | 
Default Value         | Required |
-|-------------------------------------------|----------------------------|-----------------------|----------|
+| ----------------------------------------- | -------------------------- | 
--------------------- | -------- |
 | `gravitino.lance-rest.namespace-backend`  | Namespace metadata backend | 
gravitino             | Yes      |
 | `gravitino.lance-rest.gravitino-uri`      | Gravitino server URI       | 
http://localhost:8090 | Yes      |
 | `gravitino.lance-rest.gravitino-metalake` | Gravitino metalake name    | 
(none)                | Yes      |
@@ -205,7 +212,7 @@ Access the service at `http://localhost:9101`.
 **Environment Variables:**
 
 | Environment Variable                 | Configuration Property                
    | Required | Default Value           |
-|--------------------------------------|-------------------------------------------|----------|-------------------------|
+| ------------------------------------ | 
----------------------------------------- | -------- | ----------------------- |
 | `LANCE_REST_NAMESPACE_BACKEND`       | 
`gravitino.lance-rest.namespace-backend`  | Yes      | `gravitino`             |
 | `LANCE_REST_GRAVITINO_METALAKE_NAME` | 
`gravitino.lance-rest.gravitino-metalake` | Yes      | (none)                  |
 | `LANCE_REST_GRAVITINO_URI`           | `gravitino.lance-rest.gravitino-uri`  
    | Yes      | `http://localhost:8090` |
@@ -271,9 +278,138 @@ URL encoded:        lance_catalog%24schema%24table01
 - Supports only **two levels of namespaces** before tables
 - Tables **cannot** be nested deeper than schema level  
 - Parent catalog must be created in Gravitino before using Lance REST API
-- Namespace deletion is recursive and irreversible
+- Namespace deletion defaults to `restrict`; use `cascade` to remove child 
metadata
 :::
 
+## Authentication and authorization
+
+### Authentication and deployment modes
+
+Lance REST uses Gravitino's `gravitino.authenticators` configuration for 
incoming requests in
+both auxiliary and standalone mode. See 
[Authentication](./security/how-to-authenticate.md) for
+configuring the authenticators and their credentials. Health check endpoints 
bypass authentication.
+Authentication errors use the Lance JSON error format; unsupported credentials 
return HTTP `401`.
+In standalone mode, backend authentication and authorization failures retain 
HTTP `401` and `403`
+respectively. Authentication/authorization failures do not include internal 
stack traces in `detail`.
+Unexpected failures return HTTP `500` with a generic message; the server logs 
retain the exception
+for diagnosis.
+
+| Mode                               | Identity used for Gravitino metadata 
operations                                                                      
                                         | Metadata authorization               
                                                                                
                  |
+| ---------------------------------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 | 
--------------------------------------------------------------------------------------------------------------------------------------
 |
+| Auxiliary (running with Gravitino) | Authenticated caller, including active 
roles; anonymous requests fall back to 
`gravitino.lance-rest.gravitino-simple.user-name` (default `lance-rest-server`) 
| Enabled by `gravitino.authorization.enable=true` with a configured metalake   
                                                         |
+| Standalone                         | Configured service credentials 
(`gravitino.lance-rest.gravitino-auth-type` and its simple/OAuth2 settings)     
                                               | No Lance REST per-user 
metadata authorization; the remote Gravitino server checks the service identity 
if its authorization is enabled |
+
+The auxiliary fallback applies only after authentication accepts an anonymous 
request. It does
+not recover a rejected authentication attempt. Authenticated callers keep 
their own privileges,
+active roles, ownership and audit identity; they do not inherit the service 
user's privileges.
+The fallback service user itself needs the privileges required by the 
requested operation.
+Setting `gravitino.lance-rest.gravitino-simple.user-name` explicitly is 
optional in auxiliary mode;
+configure it only to override the default anonymous fallback identity, 
`lance-rest-server`.
+
+With `simple` authentication, a Basic header supplies a user name without 
validating a password,
+and a request without credentials is accepted as anonymous. Some malformed 
Basic credentials
+also resolve to anonymous. Use an authenticator that validates credentials 
when caller identity
+must be verified; `simple` is not password authentication.
+
+Standalone authenticates incoming requests, but does not forward their 
identities or active roles
+to its Gravitino backend. All callers use the configured backend service 
identity. Standalone
+per-user authorization and scoped temporary credential vending are outside the 
supported scope. The
+backend service identity needs privileges for all underlying Gravitino calls, 
including existence
+checks performed before mutations (for example, catalog access before creating 
a namespace).
+
+### Enable auxiliary metadata authorization
+
+Configure `${GRAVITINO_HOME}/conf/gravitino.conf`:
+
+```properties
+gravitino.auxService.names = lance-rest
+gravitino.lance-rest.gravitino-metalake = my_metalake
+gravitino.authorization.enable = true
+gravitino.authorization.serviceAdmins = adminUser
+# Development example: simple accepts the supplied user name without password 
validation.
+gravitino.authenticators = simple
+```
+
+Create the metalake, add users, and grant roles through the Gravitino API as 
described in
+[Access Control](./security/access-control.md). The Lance service exposes this 
configured metalake:
+a one-level namespace identifies a catalog, a two-level namespace identifies a 
schema, and a
+three-level table identifier identifies a table.
+
+Requests may set `X-Gravitino-Active-Roles` to `ALL` (also the default when 
omitted), `NONE`, or
+a comma-separated list of assigned role names. This selection reaches both 
operation checks
+and listing filters. Malformed selections return `400`; selecting an 
unassigned role is forbidden.
+Ownership is independent of role selection, so `NONE` does not remove 
ownership privileges.
+
+### Required privileges
+
+The following rules use the same Gravitino privileges and ownership rules as
+[Iceberg REST authorization](./iceberg-rest-service.md). Privileges can be 
inherited from
+ancestor scopes as described in Access Control. Service administrators and 
metalake owners
+can operate throughout the metalake; catalog owners can operate within their 
catalogs.
+Schema owners additionally need `USE_CATALOG`, and table owners need 
`USE_CATALOG` and
+`USE_SCHEMA`. The ownership alternatives below include these ancestor owners.
+
+| Namespace operation                                                          
                        | Required privileges or ownership                      
                 |
+| 
----------------------------------------------------------------------------------------------------
 | ---------------------------------------------------------------------- |
+| `ListNamespaces` at root                                                     
                        | Membership in the metalake; returns only accessible 
catalogs           |
+| `ListNamespaces` under a catalog; `DescribeNamespace` for a catalog; 
`NamespaceExists` for a catalog | `USE_CATALOG`, or ownership                   
                         |
+| `ListNamespaces` under a schema; `DescribeNamespace` for a schema; 
`ListTables`                      | `USE_CATALOG` and `USE_SCHEMA`, or 
ownership                           |
+| `NamespaceExists` for a schema                                               
                        | `USE_CATALOG` and either `USE_SCHEMA` or 
`CREATE_SCHEMA`, or ownership |
+| `CreateNamespace` for a catalog (`create`, `exist_ok`)                       
                        | `CREATE_CATALOG` on the metalake, or metalake 
ownership                |
+| `CreateNamespace` for a schema (`create`, `exist_ok`)                        
                        | `USE_CATALOG` and `CREATE_SCHEMA`, or 
catalog/metalake ownership       |
+| `CreateNamespace` (`overwrite`); `DropNamespace`                             
                        | Ownership of the namespace or an ancestor             
                 |
+
+| Table operation                                                              
    | Required privileges or ownership                                          
                               |
+| 
--------------------------------------------------------------------------------
 | 
--------------------------------------------------------------------------------------------------------
 |
+| `DescribeTable`                                                              
    | `USE_CATALOG`, `USE_SCHEMA`, and either `SELECT_TABLE` or `MODIFY_TABLE`, 
or ownership                   |
+| `TableExists`                                                                
    | Same as `DescribeTable`, or `USE_CATALOG`, `USE_SCHEMA`, and either 
`PROBE_TABLE_LIKE` or `CREATE_TABLE` |
+| `CreateTable` (`create`, `exist_ok`); `RegisterTable` (`create`); 
`DeclareTable` | `USE_CATALOG`, `USE_SCHEMA`, and `CREATE_TABLE`, or 
schema/ancestor ownership                            |
+| `CreateTable` or `RegisterTable` (`overwrite`); `AlterColumns`; 
`DropColumns`    | `USE_CATALOG`, `USE_SCHEMA`, and `MODIFY_TABLE`, or 
ownership                                            |
+| `DropTable`; `DeregisterTable`                                               
    | Ownership of the table or an ancestor                                     
                               |
+
+`CREATE_TABLE` and `PROBE_TABLE_LIKE` authorize `TableExists` without 
authorizing
+`DescribeTable`. `CREATE_TABLE` alone does not authorize overwrite requests. 
The `DropTable`
+and `DeregisterTable` endpoints require ownership rather than `MODIFY_TABLE`. 
Similarly, namespace creation privileges do not authorize
+overwriting or dropping another owner's namespace. Successful creation assigns 
ownership to
+the effective caller.
+
+### Listings and concealed objects
+
+Namespace listings omit inaccessible catalogs and schemas. Table listings omit 
tables for which
+the caller has neither ownership nor `SELECT_TABLE`/`MODIFY_TABLE`. Filtering 
happens before
+pagination; hidden entries do not consume page slots. Access to the parent is 
checked separately.
+
+When a caller lacks an endpoint's required privileges, the request returns 
`403`, whether or not
+the target exists, without returning its stored metadata or location. An 
authorized caller can
+distinguish an existing object from a missing one (`404`). Concealment 
therefore does not mean that every
+inaccessible object returns `404`.
+
+Authorization governs metadata requests; engines access Lance data files 
directly. Responses can
+include configured storage credentials: namespace descriptions resolve secret 
properties, and
+table descriptions, creation and declaration responses return effective 
`storage_options` from
+catalog defaults and table overrides. These are shared configured credentials, 
not temporary
+credentials restricted to the caller's table privileges. Access to data files 
depends on the
+permissions of those credentials. Per-user, scoped credential vending is not 
implemented.
+
+### Authorization differences between deployment modes
+
+:::warning
+Lance REST metadata authorization is currently supported only in **auxiliary 
mode**.
+**Standalone mode is not recommended**: its authorization decisions can differ 
from auxiliary
+mode and may produce unexpected results, even for the same user and 
privileges. Use auxiliary
+mode for deployments that require Lance REST authorization.
+:::
+
+Auxiliary mode applies the Lance endpoint authorization rules and listing 
filters described above.
+Standalone mode does not apply this authorization pipeline. Instead, the 
remote Gravitino server
+checks each underlying REST call using its own rules and the backend identity 
described above.
+These checks do not provide equivalent Lance REST authorization: an operation 
allowed in one mode
+may be denied in the other, and ownership requirements and metadata visibility 
can also differ.
+Forwarding the caller's identity alone does not eliminate these differences.
+
+Alignment of the authorization behavior is tracked in
+[#13089](https://github.com/apache/gravitino/issues/13089).
+
 ## Examples
 
 The following examples demonstrate how to interact with Lance REST service 
using different programming languages and tools.
diff --git a/lance/lance-rest-server/build.gradle.kts 
b/lance/lance-rest-server/build.gradle.kts
index 7899bad829..d55180fa15 100644
--- a/lance/lance-rest-server/build.gradle.kts
+++ b/lance/lance-rest-server/build.gradle.kts
@@ -194,6 +194,7 @@ tasks {
 
     val primaryBundleDir = 
lanceSparkBundleDirFor(primaryLanceSparkBundleVersion)
     doFirst {
+      systemProperty("lance.test.runtimeClasspath", 
sourceSets["main"].runtimeClasspath.asPath)
       val bundleJar =
         primaryBundleDir.get().asFile.listFiles()?.singleOrNull { it.extension 
== "jar" }
           ?: throw GradleException(
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 a3de0c46d6..a56ee9bc3b 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
@@ -23,8 +23,10 @@ import static 
org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
+import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.exceptions.NotFoundException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
 import org.apache.gravitino.server.web.ServerHealth;
 import org.lance.namespace.errors.ConcurrentModificationException;
 import org.lance.namespace.errors.InternalException;
@@ -68,7 +70,13 @@ public class LanceExceptionMapper implements 
ExceptionMapper<Throwable> {
   }
 
   private static LanceNamespaceException toLanceNamespaceException(String 
instance, Throwable ex) {
-    if (ex instanceof NoSuchTableException) {
+    if (ex instanceof ForbiddenException) {
+      return new PermissionDeniedException(ex.getMessage(), "", instance);
+
+    } else if (ex instanceof UnauthorizedException) {
+      return new UnauthenticatedException(ex.getMessage(), "", instance);
+
+    } else if (ex instanceof NoSuchTableException) {
       return new TableNotFoundException(ex.getMessage(), getStackTrace(ex), 
instance);
 
     } else if (ex instanceof NotFoundException) {
@@ -86,7 +94,7 @@ public class LanceExceptionMapper implements 
ExceptionMapper<Throwable> {
 
     } else {
       LOG.warn("Lance REST server unexpected exception:", ex);
-      return new InternalException(ex.getMessage(), getStackTrace(ex), 
instance);
+      return new InternalException("Internal server error", "", instance);
     }
   }
 
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
index 88b3d645ec..3ec6e9c903 100644
--- 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
@@ -18,8 +18,6 @@
  */
 package org.apache.gravitino.lance.service.authorization;
 
-import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
-
 import java.lang.reflect.Method;
 import java.lang.reflect.Parameter;
 import java.util.HashMap;
@@ -188,9 +186,7 @@ public class LanceMetadataAuthorizationMethodInterceptor
     String namespaceId = pathArgument(method.getParameters(), args, 
"id").orElse("");
     Exception exception;
     if (throwable instanceof ForbiddenException) {
-      exception =
-          new PermissionDeniedException(
-              throwable.getMessage(), getStackTrace(throwable), namespaceId);
+      exception = new PermissionDeniedException(throwable.getMessage(), "", 
namespaceId);
     } else if (throwable instanceof Exception) {
       exception = (Exception) throwable;
     } else {
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
index ae10436f69..4a717aefc7 100644
--- 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
@@ -42,8 +42,7 @@ public class LanceRESTAuthInterceptionService implements 
InterceptionService {
   public static final String METALAKE_BINDING = "lanceAuthorizationMetalake";
 
   // Membership here only routes a class through the interceptor; each method 
still opts in with
-  // @AuthorizationExpression, and a method without one runs unauthorized. The 
table writes
-  // (create, register, drop, alter) are still to be annotated.
+  // @AuthorizationExpression. Endpoint coverage tests ensure no REST 
operation omits it.
   private static final Set<String> INTERCEPTED_CLASSES =
       ImmutableSet.of(
           LanceNamespaceOperations.class.getName(), 
LanceTableOperations.class.getName());
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
index 5226a20beb..ae4bffe98a 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
@@ -18,16 +18,21 @@
  */
 package org.apache.gravitino.lance.integration.test;
 
+import java.io.Writer;
 import java.net.URI;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.authorization.Privileges;
@@ -35,14 +40,20 @@ import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.authorization.SecurableObjects;
 import org.apache.gravitino.client.GravitinoMetalake;
 import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.HttpUtils;
+import org.apache.gravitino.lance.server.GravitinoLanceRESTServer;
+import org.apache.gravitino.rest.RESTUtils;
 import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.awaitility.Awaitility;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 import org.lance.namespace.model.CreateNamespaceRequest;
 import org.lance.namespace.model.DescribeNamespaceResponse;
 import org.lance.namespace.model.DropNamespaceRequest;
+import org.lance.namespace.model.ErrorResponse;
 import org.lance.namespace.model.ListNamespacesResponse;
 
 /** Verifies namespace authorization and list filtering through auxiliary-mode 
Lance REST. */
@@ -70,6 +81,7 @@ public class LanceNamespaceAuthorizationIT extends BaseIT {
     customConfigs.put(Configs.SERVICE_ADMINS.getKey(), ADMIN);
     customConfigs.put(Configs.AUTHENTICATORS.getKey(), "simple");
     customConfigs.put("SimpleAuthUserName", ADMIN);
+    customConfigs.put("gravitino.lance-rest.gravitino-simple.user-name", 
WRITER);
     super.startIntegrationTest();
 
     String metalakeName = getLanceRESTServerMetalakeName();
@@ -181,6 +193,252 @@ public class LanceNamespaceAuthorizationIT extends BaseIT 
{
     assertStatus(403, drop(USER, HIDDEN_CATALOG, "skip", null));
   }
 
+  /** Verifies that active roles survive authentication and reach namespace 
authorization. */
+  @Test
+  public void testActiveRolesReachAuthorizationAndListingFilters() throws 
Exception {
+    String schemaPath = "/v1/namespace/" + id(VISIBLE_CATALOG, VISIBLE_SCHEMA) 
+ "/describe";
+    assertStatus(200, sendWithRoles(USER, schemaPath, "ALL"));
+    assertStatus(403, sendWithRoles(USER, schemaPath, "NONE"));
+    assertStatus(403, sendWithRoles(USER, schemaPath, 
"lance_authz_catalog_role"));
+    assertStatus(
+        200, sendWithRoles(USER, schemaPath, 
"lance_authz_catalog_role,lance_authz_schema_role"));
+    assertStatus(403, sendWithRoles(USER, schemaPath, 
"lance_authz_writer_role"));
+
+    HttpResponse<String> response =
+        httpClient.send(
+            request(USER, "/v1/namespace/" + VISIBLE_CATALOG + "/list")
+                .setHeader(
+                    AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER, 
"lance_authz_catalog_role")
+                .GET()
+                .build(),
+            HttpResponse.BodyHandlers.ofString());
+    assertStatus(200, response);
+    Assertions.assertTrue(
+        ObjectMapperProvider.objectMapper()
+            .readValue(response.body(), ListNamespacesResponse.class)
+            .getNamespaces()
+            .isEmpty());
+    // Narrowing one request must not affect subsequent requests on the same 
server.
+    Assertions.assertEquals(List.of(VISIBLE_SCHEMA), list(USER, 
VISIBLE_CATALOG));
+  }
+
+  /** Verifies authentication failures stop before metadata writes or service 
identity fallback. */
+  @Test
+  public void testAuthenticationErrorsUseLanceJsonAndDoNotCreateMetadata() 
throws Exception {
+    String catalog = "lance_authz_rejected_auth_catalog";
+    CreateNamespaceRequest body = new CreateNamespaceRequest();
+    body.addIdItem(catalog);
+    String json = ObjectMapperProvider.objectMapper().writeValueAsString(body);
+    HttpResponse<String> unauthorized =
+        httpClient.send(
+            request(ADMIN, "/v1/namespace/" + catalog + "/create")
+                .setHeader(AuthConstants.HTTP_HEADER_AUTHORIZATION, "Bearer 
unsupported-token")
+                .POST(HttpRequest.BodyPublishers.ofString(json))
+                .build(),
+            HttpResponse.BodyHandlers.ofString());
+    assertError(401, unauthorized);
+    HttpResponse<String> malformedRoles =
+        httpClient.send(
+            request(ADMIN, "/v1/namespace/" + catalog + "/create")
+                .setHeader(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER, 
"ALL,NONE")
+                .POST(HttpRequest.BodyPublishers.ofString(json))
+                .build(),
+            HttpResponse.BodyHandlers.ofString());
+    assertError(400, malformedRoles);
+    assertStatus(404, post(ADMIN, catalog, "exists"));
+  }
+
+  /** Verifies anonymous fallback uses the service user's privileges and 
records its ownership. */
+  @Test
+  public void testServiceIdentityFallbackIsAuthorized() throws Exception {
+    String catalog = "lance_authz_fallback_catalog";
+    CreateNamespaceRequest body = new CreateNamespaceRequest();
+    body.addIdItem(catalog);
+    HttpRequest.Builder anonymous =
+        HttpRequest.newBuilder()
+            .uri(
+                URI.create(
+                    String.format("http://localhost:%d/lance";, 
getLanceRESTServerPort())
+                        + "/v1/namespace/"
+                        + catalog
+                        + "/create?delimiter=."))
+            .header("Content-Type", "application/json");
+    assertStatus(
+        200,
+        httpClient.send(
+            anonymous
+                .POST(
+                    HttpRequest.BodyPublishers.ofString(
+                        
ObjectMapperProvider.objectMapper().writeValueAsString(body)))
+                .build(),
+            HttpResponse.BodyHandlers.ofString()));
+    GravitinoMetalake metalake = 
client.loadMetalake(getLanceRESTServerMetalakeName());
+    Assertions.assertEquals(WRITER, 
metalake.loadCatalog(catalog).auditInfo().creator());
+    // Ownership is usable by the real service user after creation through 
anonymous fallback.
+    assertStatus(200, drop(WRITER, catalog, null, "cascade"));
+
+    HttpRequest denied =
+        HttpRequest.newBuilder()
+            .uri(
+                URI.create(
+                    String.format("http://localhost:%d/lance";, 
getLanceRESTServerPort())
+                        + "/v1/namespace/"
+                        + HIDDEN_CATALOG
+                        + "/describe?delimiter=."))
+            .header("Content-Type", "application/json")
+            .POST(HttpRequest.BodyPublishers.ofString("{}"))
+            .build();
+    assertStatus(403, httpClient.send(denied, 
HttpResponse.BodyHandlers.ofString()));
+    // An authenticated reader cannot borrow the fallback user's 
CREATE_CATALOG privilege.
+    assertStatus(403, create(USER, catalog, null, Map.of()));
+  }
+
+  /** Verifies standalone HTTP backend calls use service credentials rather 
than caller roles. */
+  @Test
+  public void testStandaloneUsesBackendServiceIdentity(@TempDir Path 
directory) throws Exception {
+    int port = RESTUtils.findAvailablePort(10000, 11000);
+    String catalog = "lance_authz_standalone_catalog";
+    String serviceUser = "lance_authz_standalone_user";
+    GravitinoMetalake metalake = 
client.loadMetalake(getLanceRESTServerMetalakeName());
+    metalake.addUser(serviceUser);
+    metalake.createRole(
+        "lance_authz_standalone_role",
+        new HashMap<>(),
+        List.of(
+            SecurableObjects.ofMetalake(
+                metalake.name(),
+                new ArrayList<>(
+                    List.of(Privileges.UseCatalog.allow(), 
Privileges.CreateCatalog.allow())))));
+    metalake.grantRolesToUser(List.of("lance_authz_standalone_role"), 
serviceUser);
+    Properties config = new Properties();
+    config.setProperty(Configs.AUTHENTICATORS.getKey(), "simple");
+    config.setProperty("gravitino.lance-rest.httpPort", String.valueOf(port));
+    config.setProperty(
+        "gravitino.lance-rest.gravitino-uri", "http://localhost:"; + 
getGravitinoServerPort());
+    config.setProperty("gravitino.lance-rest.gravitino-metalake", 
getLanceRESTServerMetalakeName());
+    config.setProperty("gravitino.lance-rest.gravitino-auth-type", "simple");
+    config.setProperty("gravitino.lance-rest.gravitino-simple.user-name", 
serviceUser);
+    Path configFile = directory.resolve("standalone.conf");
+    try (Writer writer = Files.newBufferedWriter(configFile)) {
+      config.store(writer, "Standalone Lance REST integration test");
+    }
+    Path logFile = directory.resolve("standalone.log");
+    // Use the production bootstrap in its own JVM: deploy mode has no local 
GravitinoEnv,
+    // while embedded mode must not share its backend environment with the 
standalone service.
+    ProcessBuilder builder =
+        new ProcessBuilder(
+                Path.of(System.getProperty("java.home"), "bin", 
"java").toString(),
+                "--add-opens=java.base/java.nio=ALL-UNNAMED",
+                "-cp",
+                System.getProperty("lance.test.runtimeClasspath"),
+                GravitinoLanceRESTServer.class.getName(),
+                configFile.toString())
+            .redirectErrorStream(true)
+            .redirectOutput(logFile.toFile());
+    builder.environment().put("GRAVITINO_TEST", "true");
+    Process standalone = builder.start();
+    try {
+      try {
+        Awaitility.await()
+            .atMost(60, TimeUnit.SECONDS)
+            .until(
+                () -> {
+                  Assertions.assertTrue(standalone.isAlive(), "Standalone 
process exited");
+                  // Namespace initialization is lazy and occurs on the first 
metadata request.
+                  return HttpUtils.isHttpServerUp(
+                      "http://localhost:"; + port + "/lance/health/live");
+                });
+      } catch (Exception | AssertionError e) {
+        throw new AssertionError("Standalone startup failed:\n" + 
Files.readString(logFile), e);
+      }
+      CreateNamespaceRequest body = new CreateNamespaceRequest();
+      body.addIdItem(catalog);
+      HttpRequest request =
+          request(USER, "/v1/namespace/" + catalog + "/create")
+              .uri(
+                  URI.create(
+                      "http://localhost:";
+                          + port
+                          + "/lance/v1/namespace/"
+                          + catalog
+                          + "/create?delimiter=."))
+              .setHeader(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER, "NONE")
+              .POST(
+                  HttpRequest.BodyPublishers.ofString(
+                      
ObjectMapperProvider.objectMapper().writeValueAsString(body)))
+              .build();
+      // USER cannot create catalogs in auxiliary mode. The backend receives 
the service user's
+      // credentials and roles, despite USER selecting NONE on this incoming 
request.
+      assertStatus(200, httpClient.send(request, 
HttpResponse.BodyHandlers.ofString()));
+      Assertions.assertEquals(
+          serviceUser,
+          client
+              .loadMetalake(getLanceRESTServerMetalakeName())
+              .loadCatalog(catalog)
+              .auditInfo()
+              .creator());
+      // The backend service user cannot read this admin-owned schema. Even an 
incoming admin
+      // must receive the backend's 403, rather than 500 or the incoming 
caller's privileges.
+      HttpRequest deniedRequest =
+          request(ADMIN, "/v1/namespace/" + id(VISIBLE_CATALOG, 
VISIBLE_SCHEMA) + "/describe")
+              .uri(
+                  URI.create(
+                      "http://localhost:";
+                          + port
+                          + "/lance/v1/namespace/"
+                          + id(VISIBLE_CATALOG, VISIBLE_SCHEMA)
+                          + "/describe?delimiter=."))
+              .POST(HttpRequest.BodyPublishers.ofString("{}"))
+              .build();
+      HttpResponse<String> deniedResponse =
+          httpClient.send(deniedRequest, HttpResponse.BodyHandlers.ofString());
+      assertStatus(403, deniedResponse);
+      ErrorResponse error =
+          ObjectMapperProvider.objectMapper().readValue(deniedResponse.body(), 
ErrorResponse.class);
+      Assertions.assertEquals("", error.getDetail());
+      Assertions.assertTrue(error.getError().contains(serviceUser), 
error.getError());
+      assertStatus(200, drop(serviceUser, catalog, null, "cascade"));
+    } finally {
+      standalone.destroy();
+      if (!standalone.waitFor(10, TimeUnit.SECONDS)) {
+        standalone.destroyForcibly();
+        Assertions.assertTrue(
+            standalone.waitFor(10, TimeUnit.SECONDS), "Standalone process did 
not stop");
+      }
+    }
+  }
+
+  /** Verifies health endpoints remain reachable even when credentials would 
be rejected. */
+  @Test
+  public void testHealthBypassesAuthentication() throws Exception {
+    HttpRequest health =
+        request(USER, "/health")
+            .setHeader(AuthConstants.HTTP_HEADER_AUTHORIZATION, "Bearer 
unsupported-token")
+            .GET()
+            .build();
+    assertStatus(200, httpClient.send(health, 
HttpResponse.BodyHandlers.ofString()));
+  }
+
+  private HttpResponse<String> sendWithRoles(String user, String path, String 
roles)
+      throws Exception {
+    return httpClient.send(
+        request(user, path)
+            .setHeader(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER, roles)
+            .POST(HttpRequest.BodyPublishers.ofString("{}"))
+            .build(),
+        HttpResponse.BodyHandlers.ofString());
+  }
+
+  private void assertError(int status, HttpResponse<String> response) throws 
Exception {
+    assertStatus(status, response);
+    Assertions.assertTrue(
+        
response.headers().firstValue("Content-Type").orElse("").startsWith("application/json"));
+    ErrorResponse error =
+        ObjectMapperProvider.objectMapper().readValue(response.body(), 
ErrorResponse.class);
+    Assertions.assertEquals(status, error.getCode());
+    Assertions.assertFalse(error.getError().isEmpty());
+  }
+
   private void grant(GravitinoMetalake metalake, String role, SecurableObject 
object) {
     metalake.createRole(role, new HashMap<>(), List.of(object));
     metalake.grantRolesToUser(List.of(role), USER);
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 fd3d98ded6..8f4513fdf6 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
@@ -24,6 +24,8 @@ import javax.ws.rs.Path;
 import javax.ws.rs.core.Application;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
 import org.apache.gravitino.rest.RESTUtils;
 import org.glassfish.jersey.jackson.JacksonFeature;
 import org.glassfish.jersey.server.ResourceConfig;
@@ -31,6 +33,7 @@ import org.glassfish.jersey.test.JerseyTest;
 import org.glassfish.jersey.test.TestProperties;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.lance.namespace.errors.InvalidInputException;
 import org.lance.namespace.model.ErrorResponse;
 
 /** Tests for {@link LanceExceptionMapper}. */
@@ -79,12 +82,57 @@ public class TestLanceExceptionMapper extends JerseyTest {
       Assertions.assertEquals(
           Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response.getStatus());
       ErrorResponse entity = response.readEntity(ErrorResponse.class);
-      Assertions.assertEquals("assertion failure", entity.getError());
+      Assertions.assertEquals("Internal server error", entity.getError());
       Assertions.assertEquals("", entity.getInstance());
-      Assertions.assertTrue(
-          entity.getDetail().contains("java.lang.AssertionError: assertion 
failure"));
-      Assertions.assertTrue(
-          entity.getDetail().contains("Caused by: 
java.lang.IllegalStateException: root cause"));
+      Assertions.assertEquals("", entity.getDetail());
+    }
+  }
+
+  /** Verifies backend authorization failures use the Lance forbidden 
response. */
+  @Test
+  public void testBackendForbidden() {
+    assertAuthenticationError(new ForbiddenException("Access denied"), 403);
+  }
+
+  /** Verifies backend authentication failures use the Lance unauthenticated 
response. */
+  @Test
+  public void testBackendUnauthorized() {
+    assertAuthenticationError(new UnauthorizedException("Invalid 
credentials"), 401);
+  }
+
+  /** Verifies unexpected exceptions do not expose internal details in the 
response. */
+  @Test
+  public void testInternalFailureDoesNotExposeException() {
+    try (Response response =
+        LanceExceptionMapper.toRESTResponse(
+            "catalog.schema.table", new 
RuntimeException("private-backend-detail"))) {
+      Assertions.assertEquals(500, response.getStatus());
+      ErrorResponse error = (ErrorResponse) response.getEntity();
+      Assertions.assertEquals("Internal server error", error.getError());
+      Assertions.assertEquals("", error.getDetail());
+    }
+  }
+
+  /** Verifies intentional protocol validation details remain available to 
callers. */
+  @Test
+  public void testProtocolValidationDetailsArePreserved() {
+    try (Response response =
+        LanceExceptionMapper.toRESTResponse(
+            "table",
+            new InvalidInputException("Invalid field", "field must be 
positive", "table"))) {
+      Assertions.assertEquals(400, response.getStatus());
+      Assertions.assertEquals(
+          "field must be positive", ((ErrorResponse) 
response.getEntity()).getDetail());
+    }
+  }
+
+  private void assertAuthenticationError(Exception exception, int status) {
+    try (Response response = LanceExceptionMapper.toRESTResponse("catalog", 
exception)) {
+      Assertions.assertEquals(status, response.getStatus());
+      ErrorResponse error = (ErrorResponse) response.getEntity();
+      Assertions.assertEquals(exception.getMessage(), error.getError());
+      Assertions.assertEquals("", error.getDetail());
+      Assertions.assertEquals("catalog", error.getInstance());
     }
   }
 }
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
index a80c70a06b..87069562bd 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
@@ -208,10 +208,9 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
 
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
     Assertions.assertEquals(18, errorResp.getCode());
-    Assertions.assertEquals("Test exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
     Assertions.assertEquals("ns1.ns2", errorResp.getInstance());
-    Assertions.assertNotNull(errorResp.getDetail());
-    Assertions.assertTrue(errorResp.getDetail().contains("Test exception"));
 
     // root endpoint should use explicit root identifier instead of delimiter 
in error instance
     resp =
@@ -262,7 +261,8 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
 
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
     Assertions.assertEquals(18, errorResp.getCode());
-    Assertions.assertEquals("Test exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 
   @Test
@@ -321,7 +321,8 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
 
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
     Assertions.assertEquals(18, errorResp.getCode());
-    Assertions.assertEquals("Test exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 
   @Test
@@ -393,7 +394,8 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
 
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
     Assertions.assertEquals(18, errorResp.getCode());
-    Assertions.assertEquals("Test exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 
   @Test
@@ -457,7 +459,8 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
         Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
-    Assertions.assertEquals("Runtime exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 
   @Test
@@ -513,7 +516,8 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
         Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
-    Assertions.assertEquals("Runtime exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 
   @Test
@@ -620,7 +624,8 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
         Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
-    Assertions.assertEquals("Runtime exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 
   @Test
@@ -677,7 +682,8 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
         Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
-    Assertions.assertEquals("Runtime exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 
   @Test
@@ -999,6 +1005,7 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
         Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp.getStatus());
     Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
     ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
-    Assertions.assertEquals("Runtime exception", errorResp.getError());
+    Assertions.assertEquals("Internal server error", errorResp.getError());
+    Assertions.assertEquals("", errorResp.getDetail());
   }
 }

Reply via email to