aboueleyes opened a new issue, #17759:
URL: https://github.com/apache/iceberg/issues/17759

   ### Apache Iceberg version
   
   1.11.0 (latest release)
   
   ### Query engine
   
   None
   
   ### Please describe the bug 🐞
   
   The Java REST client uses form encoding for namespace levels, table names, 
view names, and scan plan IDs placed in URL paths. A space therefore becomes 
`+` instead of `%20`. Project Nessie treats the plus sign as a literal 
character in these paths, making names containing spaces unreachable through 
the Java client.
   
   This was reproduced for namespace and table names. View names and scan plan 
IDs are affected by the same path construction but were not exercised against a 
live server.
   
   The problem remains on Iceberg `main` at commit
   
[`900e88215797a84adcadd3e0d53f7c8e0d17455f`](https://github.com/apache/iceberg/commit/900e88215797a84adcadd3e0d53f7c8e0d17455f).
   
   ### Root cause
   
   
[`RESTUtil.encodeString`](https://github.com/apache/iceberg/blob/900e88215797a84adcadd3e0d53f7c8e0d17455f/core/src/main/java/org/apache/iceberg/rest/RESTUtil.java#L140-L143)
 uses `java.net.URLEncoder`:
   
   ```java
   public static String encodeString(String toEncode) {
     Preconditions.checkArgument(toEncode != null, "Invalid string to encode: 
null");
     return URLEncoder.encode(toEncode, StandardCharsets.UTF_8);
   }
   ```
   
   `URLEncoder` implements `application/x-www-form-urlencoded`, where a space 
becomes `+`. That is correct when 
[`encodeFormData`](https://github.com/apache/iceberg/blob/900e88215797a84adcadd3e0d53f7c8e0d17455f/core/src/main/java/org/apache/iceberg/rest/RESTUtil.java#L113-L123)
   constructs an OAuth form body. It is not correct when 
[`ResourcePaths`](https://github.com/apache/iceberg/blob/900e88215797a84adcadd3e0d53f7c8e0d17455f/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java#L80-L199)
 places the result in a URL path.
   
   The current path construction uses the form encoder for:
   
   - namespace levels, through `RESTUtil.encodeNamespace`
   - table names in table, metrics, signing, planning, and task paths
   - view names
   - scan plan IDs
   
   For example:
   
   ```java
   public String table(TableIdentifier ident) {
     return SLASH.join(
         "v1",
         prefix,
         "namespaces",
         pathEncode(ident.namespace()),
         "tables",
         RESTUtil.encodeString(ident.name()));
   }
   ```
   
   The 
current[`TestResourcePaths.cancelPlanEndpointPath`](https://github.com/apache/iceberg/blob/900e88215797a84adcadd3e0d53f7c8e0d17455f/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java#L316-L337)
 also expects `plan with spaces` to become `plan+with+spaces` inside a path.
   
   ### Reproduction
   
   Environment:
   
   - Project Nessie REST catalog at `http://localhost:19120`
   - catalog prefix `main|warehouse`
   - namespace `sales report`
   - table `q1 results`
   - both objects were created through the catalog API before these checks
   - `demo`, containing two tables, was used as a control namespace
   
   First verify that the namespace exists:
   
   ```shell
   curl -s "http://localhost:19120/iceberg/v1/main%7Cwarehouse/namespaces";
   ```
   
   The response includes `sales report`.
   
   Request the namespace and table with a percent-encoded space and then with a 
plus sign. The path suffixes below differ only in that encoding:
   
   ```text
   namespaces/sales%20report                      -> 200
   namespaces/sales+report                        -> 404
   
   namespaces/sales%20report/tables/q1%20results  -> 200
   namespaces/sales%20report/tables/q1+results    -> 404
   namespaces/sales+report/tables/q1%20results    -> 404
   namespaces/sales+report/tables/q1+results      -> 404
   ```
   
   `ResourcePaths` constructs:
   
   ```text
   namespace path : v1/main|warehouse/namespaces/sales+report
   tables path    : v1/main|warehouse/namespaces/sales+report/tables
   table path     : v1/main|warehouse/namespaces/sales+report/tables/q1+results
   ```
   
   End-to-end through `RESTCatalog`:
   
   ```text
   listTables(demo)          -> 2 tables
   listTables(sales report)  -> 0 tables
   loadTable(q1 results)     -> NoSuchTableException: Table does not exist: 
sales+report.q1+results
   ```
   
   The empty `listTables` result is server-dependent. Nessie returns `200` and 
an empty identifiers array for the unknown `sales+report` tables path. Another 
server may return `404`. In both cases, the Java client sends a path for a 
different name than the one requested.
   
   ### Expected behavior
   
   Namespace levels, table names, view names, and scan plan IDs should use 
URL-path encoding. A space should be sent as `%20`, while a literal plus sign 
should be sent as `%2B`.
   
   OAuth form bodies must retain form encoding, where a space is represented by 
`+`.
   
   ### Proposed fix
   
   Add a path-specific encoder:
   
   ```java
   public static String encodePathSegment(String toEncode) {
     Preconditions.checkArgument(toEncode != null, "Invalid string to encode: 
null");
     return URLEncoder.encode(toEncode, StandardCharsets.UTF_8).replace("+", 
"%20");
   }
   ```
   
   Then:
   
   1. Use `encodePathSegment` for each namespace level in `encodeNamespace`.
   2. Replace direct `encodeString` calls for table names, view names, and scan 
plan IDs in `ResourcePaths`.
   3. Keep form encoding in `encodeFormData`.
   4. The change preserves existing Iceberg namespace decoding, which accepts 
both legacy + and new %20 representations. Correct URL-path decoders accept the 
new representation. Compatibility with catalogs that depend specifically on + 
in path segments requires validation.
   
   The exact method name and visibility are open for maintainer guidance. This 
direction follows the review recommendation on #15948.
   
   ### Suggested regression coverage
   
   - namespace levels containing spaces
   - table and view names containing spaces
   - scan plan IDs containing spaces
   - a name containing both a literal plus and a space, such as `my+name 
with+spaces`
   - multipart namespaces using the configured namespace separator
   - legacy namespace decoding from `my+namespace`
   - `encodeFormData` continuing to encode spaces as `+`
   - all affected paths constructed by `ResourcePaths`
   
   For example, `my+name with+spaces` should be encoded as 
`my%2Bname%20with%2Bspaces` when used as one path segment.
   
   ### Compatibility considerations
   
   - The outgoing representation of a space in the affected path segments 
changes from `+` to `%20`.
   - A literal plus sign remains `%2B`.
   - OAuth form-body encoding remains unchanged.
   - `URLDecoder`, which is currently used by the namespace helper, decodes 
both `%20` and `+` as a space.
   - Correct URL-path decoders accept `%20`. Catalog-specific behavior outside 
the reproduced Nessie case has not been tested here.
   
   ### Related work
   
   - [#12308](https://github.com/apache/iceberg/issues/12308) reported form 
encoding in path parameters
     and was closed automatically after becoming stale.
   - [#14263](https://github.com/apache/iceberg/issues/14263) reproduced the 
namespace symptom with
     Polaris and Spark. Maintainers requested an Iceberg reproduction and 
discussed the client/server boundary. It was later closed automatically after 
becoming stale.
   - [#15948](https://github.com/apache/iceberg/pull/15948) attempted a 
namespace-only fix. Review requested a separate path encoder, literal-plus and 
legacy-decoding tests, an OAuth form-data test, and coverage for all affected 
`ResourcePaths`. The PR was closed automatically for inactivity
     before those comments were addressed.
   
   References:
   
   - [Java 
`URLEncoder`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/URLEncoder.html)
   - [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html)
   
   
   ### Willingness to contribute
   
   - [x] I can contribute a fix for this bug independently
   - [ ] I would be willing to contribute a fix for this bug with guidance from 
the Iceberg community
   - [ ] I cannot contribute a fix for this bug at this time


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to