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

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new bfe44d5553b HDDS-15259. PutObject should treat null tag value for 
x-amz-tagging header as empty one (#10299)
bfe44d5553b is described below

commit bfe44d5553b38eb18cd350d57d706dbf5fbe0a1d
Author: Gargi Jaiswal <[email protected]>
AuthorDate: Mon May 18 14:42:00 2026 +0530

    HDDS-15259. PutObject should treat null tag value for x-amz-tagging header 
as empty one (#10299)
---
 .../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java   | 36 ++++++++++++++++++++++
 .../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java   | 34 ++++++++++++++++++++
 .../hadoop/ozone/s3/endpoint/EndpointBase.java     | 14 +++++++--
 .../hadoop/ozone/s3/endpoint/TestObjectPut.java    | 26 ++++++++++++----
 4 files changed, 102 insertions(+), 8 deletions(-)

diff --git 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
index f1c47df8557..0e5ed3e616e 100644
--- 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
+++ 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
@@ -1105,6 +1105,42 @@ public void testGetObject() throws Exception {
     }
   }
 
+  static Stream<Arguments> onlyTagKeyCasesV1() {
+    Map<String, String> fooBarEmptyBar = new HashMap<>();
+    fooBarEmptyBar.put("foo", "bar");
+    fooBarEmptyBar.put("bar", "");
+    return Stream.of(
+        Arguments.of(
+            new ObjectTagging(Collections.singletonList(new Tag("tag1", 
null))),
+            Collections.singletonMap("tag1", "")),
+        Arguments.of(
+            new ObjectTagging(Arrays.asList(new Tag("foo", "bar"), new 
Tag("bar", null))),
+            fooBarEmptyBar)
+    );
+  }
+
+  @ParameterizedTest
+  @MethodSource("onlyTagKeyCasesV1")
+  public void testPutObjectWithOnlyTagKey(ObjectTagging objectTagging,
+      Map<String, String> expectedTags) throws Exception {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    final String content = "0123456789";
+    s3Client.createBucket(bucketName);
+
+    try (InputStream is = new 
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
+      PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, 
keyName, is, new ObjectMetadata())
+          .withTagging(objectTagging);
+      s3Client.putObject(putObjectRequest);
+    }
+
+    GetObjectTaggingResult taggingResult = s3Client.getObjectTagging(
+        new GetObjectTaggingRequest(bucketName, keyName));
+    Map<String, String> actualTags = taggingResult.getTagSet().stream()
+        .collect(Collectors.toMap(Tag::getKey, Tag::getValue));
+    assertEquals(expectedTags, actualTags);
+  }
+
   @Test
   public void testHeadObjectReturnsTaggingCount() {
     final String bucketName = getBucketName();
diff --git 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
index 7b12bc6f4dd..3a1df68154c 100644
--- 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
+++ 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
@@ -244,6 +244,40 @@ public void testPutObject() {
     assertEquals("\"37b51d194a7513e45b56f6524f2d51f2\"", 
getObjectResponse.eTag());
   }
 
+  static Stream<Arguments> onlyTagKeyCasesV2() {
+    Map<String, String> fooBarEmptyBar = new HashMap<>();
+    fooBarEmptyBar.put("foo", "bar");
+    fooBarEmptyBar.put("bar", "");
+    return Stream.of(
+        Arguments.of("tag1", Collections.singletonMap("tag1", "")),
+        Arguments.of("foo=bar&bar", fooBarEmptyBar)
+    );
+  }
+
+  @ParameterizedTest
+  @MethodSource("onlyTagKeyCasesV2")
+  public void testPutObjectWithOnlyTagKey(String taggingHeader,
+      Map<String, String> expectedTags) {
+    final String bucketName = getBucketName();
+    final String keyName = getKeyName();
+    final String content = "0123456789";
+    s3Client.createBucket(b -> b.bucket(bucketName));
+
+    PutObjectRequest request = PutObjectRequest.builder()
+        .bucket(bucketName)
+        .key(keyName)
+        .tagging(taggingHeader)
+        .build();
+    s3Client.putObject(request, RequestBody.fromString(content));
+
+    Map<String, String> actualTags = s3Client.getObjectTagging(
+            b -> b.bucket(bucketName).key(keyName))
+        .tagSet()
+        .stream()
+        .collect(Collectors.toMap(Tag::key, Tag::value));
+    assertEquals(expectedTags, actualTags);
+  }
+
   @Test
   public void testGetObjectTaggingReturnsTagsSortedByKey() {
     final String bucketName = getBucketName();
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index 649b14b49cd..e9c7f2c9fa8 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -363,7 +363,16 @@ protected Map<String, String> 
getTaggingFromHeaders(HttpHeaders httpHeaders)
 
     List<NameValuePair> tagPairs = URLEncodedUtils.parse(tagString, UTF_8);
 
-    return validateAndGetTagging(tagPairs, NameValuePair::getName, 
NameValuePair::getValue);
+    // Put Object with x-amz-tagging header. A segment with no '=' 
(e.g."foo=bar&bar") is
+    // typically represented as (key=bar, value=null). AWS S3 treats that as 
an empty value for "bar".
+    // We map null → "" here only for this header path.
+    // PutObjectTagging is different: the XML/JSON API requires each Tag to
+    // include a Value element; so a missing Value stays null and fails 
validation.
+    return validateAndGetTagging(tagPairs, NameValuePair::getName,
+        pair -> {
+          String v = pair.getValue();
+          return v != null ? v : "";
+        });
   }
 
   protected static <KV> Map<String, String> validateAndGetTagging(
@@ -389,7 +398,8 @@ protected static <KV> Map<String, String> 
validateAndGetTagging(
       }
 
       if (tagValue == null) {
-        // For example for query parameter with only value (e.g. "tag1")
+        // Missing tag value is invalid for PutObjectTagging XML/JSON; 
x-amz-tagging must
+        // normalize null to "" in getTaggingFromHeaders before calling this 
method.
         OS3Exception ex = S3ErrorTable.newError(INVALID_TAG, tagKey);
         ex.setErrorMessage("Some tag values are not specified, please specify 
the tag values");
         throw ex;
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
index 185d126ccec..bcd4cdd9084 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectPut.java
@@ -174,13 +174,27 @@ public void testPutObjectWithTags() throws Exception {
     assertEquals("value2", tags.get("tag2"));
   }
 
-  @Test
-  public void testPutObjectWithOnlyTagKey() {
-    // Try to send with only the key (no value)
-    when(headers.getHeaderString(TAG_HEADER)).thenReturn("tag1");
+  static Stream<Arguments> onlyTagKeyCases() {
+    return Stream.of(
+        Arguments.of("tag1", ImmutableMap.of("tag1", "")),
+        Arguments.of("foo=bar&bar", ImmutableMap.of("foo", "bar", "bar", ""))
+    );
+  }
 
-    OS3Exception ex = assertErrorResponse(INVALID_TAG, () -> 
putObject(CONTENT));
-    assertThat(ex.getErrorMessage()).contains("Some tag values are not 
specified");
+  /**
+   * Put Object with {@code x-amz-tagging} header where key with a null value 
is treated as
+   * an empty string value (AWS), e.g. foo=bar&bar, here bar = " ".
+   */
+  @ParameterizedTest
+  @MethodSource("onlyTagKeyCases")
+  void testPutObjectWithOnlyTagKey(String tagHeader,
+      Map<String, String> expectedTags) throws Exception {
+    when(headers.getHeaderString(TAG_HEADER)).thenReturn(tagHeader);
+
+    assertSucceeds(() -> putObject(CONTENT));
+
+    assertThat(bucket.getKey(KEY_NAME).getTags())
+        .containsExactlyInAnyOrderEntriesOf(expectedTags);
   }
 
   @Test


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

Reply via email to