This is an automated email from the ASF dual-hosted git repository.

fanng 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 8381f2aec5 [#9936] fix(iceberg): URL decodes the table name in iceberg 
rest server paths (#9955)
8381f2aec5 is described below

commit 8381f2aec5a305c79d8c55217e2e6ae9b94c4aa9
Author: Xinyi Lu <[email protected]>
AuthorDate: Thu Feb 12 18:45:03 2026 -0800

    [#9936] fix(iceberg): URL decodes the table name in iceberg rest server 
paths (#9955)
    
    ### What changes were proposed in this pull request?
    
    This change decodes table names in the IcebergTableOperations path
    through Iceberg RestUtil.decodeString.
    
    ### Why are the changes needed?
    
    Fix: #9936
    
    ### Does this PR introduce _any_ user-facing change?
    
    No
    
    ### How was this patch tested?
    
    Through added unittest.
---
 .../service/rest/IcebergTableOperations.java       | 53 ++++++++++++++--------
 .../service/rest/IcebergViewOperations.java        | 34 +++++++++-----
 .../service/rest/TestIcebergTableOperations.java   | 35 ++++++++++++++
 .../service/rest/TestIcebergViewOperations.java    | 35 ++++++++++++++
 4 files changed, 125 insertions(+), 32 deletions(-)

diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
index e499dd8c62..e8af54b985 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
@@ -195,10 +195,12 @@ public class IcebergTableOperations {
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
           String namespace,
-      @AuthorizationMetadata(type = Entity.EntityType.TABLE) 
@PathParam("table") String table,
+      @AuthorizationMetadata(type = Entity.EntityType.TABLE) @Encoded() 
@PathParam("table")
+          String table,
       UpdateTableRequest updateTableRequest) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String tableName = RESTUtil.decodeString(table);
     if (LOG.isInfoEnabled()) {
       LOG.info(
           "Update Iceberg table, catalog: {}, namespace: {}, table: {}, 
updateTableRequest: {}",
@@ -213,7 +215,7 @@ public class IcebergTableOperations {
           () -> {
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
-            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
table);
+            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
tableName);
             LoadTableResponse loadTableResponse =
                 tableOperationDispatcher.updateTable(context, tableIdentifier, 
updateTableRequest);
             return IcebergRESTUtils.ok(loadTableResponse);
@@ -238,21 +240,23 @@ public class IcebergTableOperations {
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
           String namespace,
-      @AuthorizationMetadata(type = Entity.EntityType.TABLE) 
@PathParam("table") String table,
+      @AuthorizationMetadata(type = Entity.EntityType.TABLE) @Encoded() 
@PathParam("table")
+          String table,
       @DefaultValue("false") @QueryParam("purgeRequested") boolean 
purgeRequested) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String tableName = RESTUtil.decodeString(table);
     LOG.info(
         "Drop Iceberg table, catalog: {}, namespace: {}, table: {}, 
purgeRequested: {}",
         catalogName,
         icebergNS,
-        table,
+        tableName,
         purgeRequested);
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
-            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
table);
+            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
tableName);
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
             tableOperationDispatcher.dropTable(context, tableIdentifier, 
purgeRequested);
@@ -278,18 +282,20 @@ public class IcebergTableOperations {
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
           String namespace,
-      @IcebergAuthorizationMetadata(type = RequestType.LOAD_TABLE) 
@PathParam("table") String table,
+      @IcebergAuthorizationMetadata(type = RequestType.LOAD_TABLE) @Encoded() 
@PathParam("table")
+          String table,
       @DefaultValue("all") @QueryParam("snapshots") String snapshots,
       @HeaderParam(X_ICEBERG_ACCESS_DELEGATION) String accessDelegation) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String tableName = RESTUtil.decodeString(table);
     boolean isCredentialVending = isCredentialVending(accessDelegation);
     LOG.info(
         "Load Iceberg table, catalog: {}, namespace: {}, table: {}, access 
delegation: {}, "
             + "credential vending: {}",
         catalogName,
         icebergNS,
-        table,
+        tableName,
         accessDelegation,
         isCredentialVending);
     // todo support snapshots
@@ -297,7 +303,7 @@ public class IcebergTableOperations {
       return Utils.doAs(
           httpRequest,
           () -> {
-            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
table);
+            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
tableName);
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName, 
isCredentialVending);
             LoadTableResponse loadTableResponse =
@@ -324,21 +330,23 @@ public class IcebergTableOperations {
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
           String namespace,
-      @AuthorizationMetadata(type = Entity.EntityType.TABLE) 
@PathParam("table") String table) {
+      @AuthorizationMetadata(type = Entity.EntityType.TABLE) @Encoded() 
@PathParam("table")
+          String table) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String tableName = RESTUtil.decodeString(table);
     LOG.info(
         "Check Iceberg table exists, catalog: {}, namespace: {}, table: {}",
         catalogName,
         icebergNS,
-        table);
+        tableName);
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
-            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
table);
+            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
tableName);
             boolean exists = tableOperationDispatcher.tableExists(context, 
tableIdentifier);
             if (exists) {
               return IcebergRESTUtils.noContent();
@@ -359,15 +367,16 @@ public class IcebergTableOperations {
   public Response reportTableMetrics(
       @PathParam("prefix") String prefix,
       @Encoded() @PathParam("namespace") String namespace,
-      @PathParam("table") String table,
+      @Encoded() @PathParam("table") String table,
       ReportMetricsRequest request) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String tableName = RESTUtil.decodeString(table);
     LOG.info(
         "Report Iceberg table metrics, catalog: {}, namespace: {}, table: {}",
         catalogName,
         icebergNS,
-        table);
+        tableName);
     try {
       return Utils.doAs(
           httpRequest,
@@ -417,20 +426,21 @@ public class IcebergTableOperations {
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
           String namespace,
-      @AuthorizationMetadata(type = EntityType.TABLE) @PathParam("table") 
String table) {
+      @AuthorizationMetadata(type = EntityType.TABLE) @Encoded() 
@PathParam("table") String table) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String tableName = RESTUtil.decodeString(table);
     LOG.info(
         "Get Iceberg table credentials, catalog: {}, namespace: {}, table: {}",
         catalogName,
         icebergNS,
-        table);
+        tableName);
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
             // Convert Iceberg table identifier to Gravitino NameIdentifier
-            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
table);
+            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
tableName);
             // First check if the table exists
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
@@ -470,19 +480,22 @@ public class IcebergTableOperations {
       @PathParam("prefix") @AuthorizationMetadata(type = EntityType.CATALOG) 
String prefix,
       @Encoded() @PathParam("namespace") @AuthorizationMetadata(type = 
EntityType.SCHEMA)
           String namespace,
-      @PathParam("table") @AuthorizationMetadata(type = EntityType.TABLE) 
String table,
+      @Encoded() @PathParam("table") @AuthorizationMetadata(type = 
EntityType.TABLE) String table,
       PlanTableScanRequest scanRequest) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
-
+    String tableName = RESTUtil.decodeString(table);
     LOG.info(
-        "Plan table scan, catalog: {}, namespace: {}, table: {}", catalogName, 
icebergNS, table);
+        "Plan table scan, catalog: {}, namespace: {}, table: {}",
+        catalogName,
+        icebergNS,
+        tableName);
 
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
-            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
table);
+            TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS, 
tableName);
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
 
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
index 728a1a2ceb..bcbc77ac38 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
@@ -134,16 +134,20 @@ public class IcebergViewOperations {
   public Response loadView(
       @PathParam("prefix") String prefix,
       @Encoded() @PathParam("namespace") String namespace,
-      @PathParam("view") String view) {
+      @Encoded() @PathParam("view") String view) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String viewName = RESTUtil.decodeString(view);
     LOG.info(
-        "Load Iceberg view, catalog: {}, namespace: {}, view: {}", 
catalogName, icebergNS, view);
+        "Load Iceberg view, catalog: {}, namespace: {}, view: {}",
+        catalogName,
+        icebergNS,
+        viewName);
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
-            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
view);
+            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
viewName);
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
             LoadViewResponse loadViewResponse =
@@ -163,15 +167,16 @@ public class IcebergViewOperations {
   public Response replaceView(
       @PathParam("prefix") String prefix,
       @Encoded() @PathParam("namespace") String namespace,
-      @PathParam("view") String view,
+      @Encoded() @PathParam("view") String view,
       UpdateTableRequest replaceViewRequest) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String viewName = RESTUtil.decodeString(view);
     LOG.info(
         "Replace Iceberg view, catalog: {}, namespace: {}, view: {}, 
replaceViewRequest: {}",
         catalogName,
         icebergNS,
-        view,
+        viewName,
         SerializeReplaceViewRequest(replaceViewRequest));
     try {
       return Utils.doAs(
@@ -179,7 +184,7 @@ public class IcebergViewOperations {
           () -> {
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
-            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
view);
+            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
viewName);
             LoadViewResponse loadViewResponse =
                 viewOperationDispatcher.replaceView(context, viewIdentifier, 
replaceViewRequest);
             return IcebergRESTUtils.ok(loadViewResponse);
@@ -197,16 +202,20 @@ public class IcebergViewOperations {
   public Response dropView(
       @PathParam("prefix") String prefix,
       @Encoded() @PathParam("namespace") String namespace,
-      @PathParam("view") String view) {
+      @Encoded() @PathParam("view") String view) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String viewName = RESTUtil.decodeString(view);
     LOG.info(
-        "Drop Iceberg view, catalog: {}, namespace: {}, view: {}", 
catalogName, icebergNS, view);
+        "Drop Iceberg view, catalog: {}, namespace: {}, view: {}",
+        catalogName,
+        icebergNS,
+        viewName);
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
-            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
view);
+            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
viewName);
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
             viewOperationDispatcher.dropView(context, viewIdentifier);
@@ -225,21 +234,22 @@ public class IcebergViewOperations {
   public Response viewExists(
       @PathParam("prefix") String prefix,
       @Encoded() @PathParam("namespace") String namespace,
-      @PathParam("view") String view) {
+      @Encoded() @PathParam("view") String view) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+    String viewName = RESTUtil.decodeString(view);
     LOG.info(
         "Check Iceberg view exists, catalog: {}, namespace: {}, view: {}",
         catalogName,
         icebergNS,
-        view);
+        viewName);
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
             IcebergRequestContext context =
                 new IcebergRequestContext(httpServletRequest(), catalogName);
-            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
view);
+            TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS, 
viewName);
             boolean exists = viewOperationDispatcher.viewExists(context, 
viewIdentifier);
             if (exists) {
               return IcebergRESTUtils.noContent();
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
index f4a9a2b602..606367ecaa 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
@@ -74,6 +74,7 @@ import org.apache.iceberg.metrics.CommitReport;
 import org.apache.iceberg.metrics.ImmutableCommitMetricsResult;
 import org.apache.iceberg.metrics.ImmutableCommitReport;
 import org.apache.iceberg.rest.PlanStatus;
+import org.apache.iceberg.rest.RESTUtil;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
 import org.apache.iceberg.rest.requests.PlanTableScanRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
@@ -692,6 +693,40 @@ public class TestIcebergTableOperations extends 
IcebergNamespaceTestBase {
         "Error message should mention remote signing: " + errorBody);
   }
 
+  @ParameterizedTest
+  
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
+  void testTableOperationsWithEncodedName(Namespace namespace) {
+    // Table names with special characters that require percent-encoding in 
URL paths.
+    // RESTUtil.encodeString encodes them for the URL, and the server should 
decode
+    // them back via RESTUtil.decodeString thanks to @Encoded on the path 
parameter.
+    String[] specialNames = {"table.with.dots", "table@special"};
+
+    verifyCreateNamespaceSucc(namespace);
+
+    for (String originalName : specialNames) {
+      String encodedName = RESTUtil.encodeString(originalName);
+
+      // Create uses the original name in the request body, not in the URL path
+      verifyCreateTableSucc(namespace, originalName);
+
+      // Load, exists use the encoded name in the URL path
+      verifyLoadTableSucc(namespace, encodedName);
+      verifyTableExistsStatusCode(namespace, encodedName, 204);
+
+      // Update: load metadata with encoded path, then update with encoded path
+      TableMetadata metadata = getTableMeta(namespace, encodedName);
+      verifyUpdateSucc(namespace, encodedName, metadata);
+    }
+
+    // Verify list returns the original (decoded) table names
+    verifyListTableSucc(namespace, ImmutableSet.copyOf(specialNames));
+
+    for (String originalName : specialNames) {
+      verifyDropTableSucc(namespace, RESTUtil.encodeString(originalName));
+    }
+    verifyDropNamespaceSucc(namespace);
+  }
+
   @ParameterizedTest
   
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
   void testInvalidAccessDelegation(Namespace namespace) {
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
index a319638fa9..79df5de0b0 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
@@ -54,6 +54,7 @@ import org.apache.iceberg.Schema;
 import org.apache.iceberg.UpdateRequirements;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.RESTUtil;
 import org.apache.iceberg.rest.requests.CreateViewRequest;
 import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
@@ -260,6 +261,40 @@ public class TestIcebergViewOperations extends 
IcebergNamespaceTestBase {
     verifyRenameViewFail(namespace, "rename_foo2", "rename_foo3", 409);
   }
 
+  @ParameterizedTest
+  
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
+  void testViewOperationsWithEncodedName(Namespace namespace) {
+    // View names with special characters that require percent-encoding in URL 
paths.
+    // RESTUtil.encodeString encodes them for the URL, and the server should 
decode
+    // them back via RESTUtil.decodeString thanks to @Encoded on the path 
parameter.
+    String[] specialNames = {"view.with.dots", "view@special"};
+
+    verifyCreateNamespaceSucc(namespace);
+
+    for (String originalName : specialNames) {
+      String encodedName = RESTUtil.encodeString(originalName);
+
+      // Create uses the original name in the request body, not in the URL path
+      verifyCreateViewSucc(namespace, originalName);
+
+      // Load, exists use the encoded name in the URL path
+      verifyLoadViewSucc(namespace, encodedName);
+      verifyViewExistsStatusCode(namespace, encodedName, 204);
+
+      // Replace: load metadata with encoded path, then replace with encoded 
path
+      ViewMetadata metadata = getViewMeta(namespace, encodedName);
+      verifyReplaceSucc(namespace, encodedName, metadata);
+    }
+
+    // Verify list returns the original (decoded) view names
+    verifyLisViewSucc(namespace, ImmutableSet.copyOf(specialNames));
+
+    for (String originalName : specialNames) {
+      verifyDropViewSucc(namespace, RESTUtil.encodeString(originalName));
+    }
+    verifyDropNamespaceSucc(namespace);
+  }
+
   private Response doCreateView(Namespace ns, String name) {
     CreateViewRequest createViewRequest =
         ImmutableCreateViewRequest.builder()

Reply via email to