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

ChenSammi 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 6d15218437b HDDS-16372. Surface OM's detailed message for rejected 
lifecycle configuration (#11189)
6d15218437b is described below

commit 6d15218437ba3c617c6ccda8cea8682cefdbf165
Author: Priyesh Karatha <[email protected]>
AuthorDate: Tue Sep 15 12:57:34 2026 +0530

    HDDS-16372. Surface OM's detailed message for rejected lifecycle 
configuration (#11189)
---
 .../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java   | 21 ++++++++++
 .../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java   | 20 +++++++++
 .../ozone/s3/endpoint/BucketLifecycleHandler.java  |  5 ++-
 .../endpoint/TestS3LifecycleConfigurationPut.java  | 48 +++++++++++++++++-----
 4 files changed, 82 insertions(+), 12 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 42e8140dcb8..07dae405a12 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
@@ -2257,6 +2257,27 @@ public void testS3LifecycleConfigurationCreationFailed() 
{
     assertEquals(ErrorType.Client, ase2.getErrorType());
     assertEquals(HttpURLConnection.HTTP_NOT_FOUND, ase2.getStatusCode());
     assertEquals("NoSuchBucket", ase2.getErrorCode());
+
+    BucketLifecycleConfiguration pastDateConfig = new 
BucketLifecycleConfiguration();
+    List<BucketLifecycleConfiguration.Rule> pastDateRules = new ArrayList<>();
+    BucketLifecycleConfiguration.Rule pastDateRule = new 
BucketLifecycleConfiguration.Rule()
+        .withId("past-date")
+        .withPrefix("logs/")
+        .withStatus(BucketLifecycleConfiguration.ENABLED)
+        .withExpirationDate(Date.from(Instant.parse("2020-01-01T00:00:00Z")));
+    pastDateRules.add(pastDateRule);
+    pastDateConfig.setRules(pastDateRules);
+    SetBucketLifecycleConfigurationRequest pastDateRequest =
+        new SetBucketLifecycleConfigurationRequest(bucketName, pastDateConfig);
+
+    AmazonServiceException ase3 = assertThrows(AmazonServiceException.class,
+        () -> s3Client.setBucketLifecycleConfiguration(pastDateRequest));
+    assertEquals(ErrorType.Client, ase3.getErrorType());
+    assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, ase3.getStatusCode());
+    assertEquals(S3ErrorTable.INVALID_REQUEST.getCode(), ase3.getErrorCode());
+    assertTrue(ase3.getErrorMessage().contains("must be in the future"),
+        "The client should receive OM's detailed message explaining the date 
is in the past, got: "
+            + ase3.getErrorMessage());
   }
 
   @Test
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 bfcf5b741bc..28c4efad510 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
@@ -2785,6 +2785,26 @@ public void testS3LifecycleConfigurationCreationFailed() 
{
             .lifecycleConfiguration(validConfig)));
     assertEquals(HttpURLConnection.HTTP_NOT_FOUND, exception2.statusCode());
     assertEquals(S3ErrorTable.NO_SUCH_BUCKET.getCode(), 
exception2.awsErrorDetails().errorCode());
+
+    LifecycleRule pastDateRule = LifecycleRule.builder()
+        .id("past-date")
+        .prefix("logs/")
+        .status(ExpirationStatus.ENABLED)
+        
.expiration(LifecycleExpiration.builder().date(Instant.parse("2020-01-01T00:00:00Z")).build())
+        .build();
+    BucketLifecycleConfiguration pastDateConfig = 
BucketLifecycleConfiguration.builder()
+        .rules(pastDateRule)
+        .build();
+
+    S3Exception exception3 = assertThrows(S3Exception.class,
+        () -> s3Client.putBucketLifecycleConfiguration(b -> b
+            .bucket(bucketName)
+            .lifecycleConfiguration(pastDateConfig)));
+    assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, exception3.statusCode());
+    assertEquals(S3ErrorTable.INVALID_REQUEST.getCode(), 
exception3.awsErrorDetails().errorCode());
+    assertTrue(exception3.awsErrorDetails().errorMessage().contains("must be 
in the future"),
+        "The client should receive OM's detailed message explaining the date 
is in the past, got: "
+            + exception3.awsErrorDetails().errorMessage());
   }
 
   @Test
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
index f25ea9d8b89..ae6efe0f107 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java
@@ -142,7 +142,7 @@ public Response 
putBucketLifecycleConfiguration(S3RequestContext context, String
       // translation maps to InvalidRequest. AWS S3 uses InvalidArgument for a 
rejected lifecycle
       // configuration, so only this validation step is remapped.
       if (ex.getResult() == OMException.ResultCodes.INVALID_REQUEST) {
-        throw S3ErrorTable.newError(S3ErrorTable.INVALID_ARGUMENT, bucketName, 
ex);
+        throw S3ErrorTable.newError(S3ErrorTable.INVALID_ARGUMENT, bucketName, 
ex).withMessage(ex.getMessage());
       }
       throw S3ErrorTable.newError(bucketName, ex);
     }
@@ -152,6 +152,9 @@ public Response 
putBucketLifecycleConfiguration(S3RequestContext context, String
     } catch (OMException ex) {
       // OM raises INVALID_REQUEST for server-side conditions as well, such as 
a bucket layout
       // mismatch, so its result codes keep the shared translation instead of 
being remapped.
+      if (ex.getResult() == OMException.ResultCodes.INVALID_REQUEST) {
+        throw S3ErrorTable.newError(bucketName, 
ex).withMessage(ex.getMessage());
+      }
       throw S3ErrorTable.newError(bucketName, ex);
     }
     return Response.ok().build();
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
index e923526a0f5..1abace96ab4 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java
@@ -32,6 +32,7 @@
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doThrow;
@@ -53,6 +54,7 @@
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.OmLCExpiration;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
 import org.apache.hadoop.ozone.s3.util.S3Consts;
 import org.junit.jupiter.api.BeforeEach;
@@ -140,10 +142,13 @@ public void 
testPutLifecycleConfigurationPropagatesOmInvalidRequest()
       throws Exception {
     // OM also raises INVALID_REQUEST for conditions that are not rejected 
rule values, such as the
     // bucket layout mismatch in OMLifecycleConfigurationSetRequest. Those 
keep reporting
-    // InvalidRequest instead of being remapped to InvalidArgument.
-    assertUnhandledOMExceptionPropagated(
-        new OMException("Bucket layout mismatch", 
OMException.ResultCodes.INVALID_REQUEST),
-        HTTP_BAD_REQUEST, INVALID_REQUEST.getCode());
+    // InvalidRequest instead of being remapped to InvalidArgument, and OM's 
message is surfaced.
+    OMException omException = new OMException("Bucket layout mismatch", 
OMException.ResultCodes.INVALID_REQUEST);
+    OS3Exception ex = putLifecycleConfigurationThrowingOm(omException);
+    assertEquals(HTTP_BAD_REQUEST, ex.getHttpCode());
+    assertEquals(INVALID_REQUEST.getCode(), ex.getCode());
+    assertEquals(omException.getMessage(), ex.getErrorMessage(),
+        "OM's detailed message should be surfaced for server-side 
INVALID_REQUEST");
   }
 
   @Test
@@ -239,13 +244,21 @@ public void 
testPutLifecycleConfigurationPropagatesInternalError()
       throws Exception {
     // An unexpected internal OM failure must be reported as InternalError 
(HTTP 500),
     // not swallowed into a false HTTP 200 success.
-    assertUnhandledOMExceptionPropagated(
-        new OMException("boom", OMException.ResultCodes.INTERNAL_ERROR),
-        HTTP_INTERNAL_ERROR, INTERNAL_ERROR.getCode());
+    OMException omException = new OMException("boom", 
OMException.ResultCodes.INTERNAL_ERROR);
+    OS3Exception ex = putLifecycleConfigurationThrowingOm(omException);
+    assertEquals(HTTP_INTERNAL_ERROR, ex.getHttpCode());
+    assertEquals(INTERNAL_ERROR.getCode(), ex.getCode());
+    assertEquals(INTERNAL_ERROR.getErrorMessage(), ex.getErrorMessage());
   }
 
   private void assertUnhandledOMExceptionPropagated(OMException omException,
       int expectedHttpCode, String expectedErrorCode) throws Exception {
+    OS3Exception ex = putLifecycleConfigurationThrowingOm(omException);
+    assertEquals(expectedHttpCode, ex.getHttpCode());
+    assertEquals(expectedErrorCode, ex.getCode());
+  }
+
+  private OS3Exception putLifecycleConfigurationThrowingOm(OMException 
omException) throws Exception {
     OzoneClient mockClient = mock(OzoneClient.class);
     ObjectStore mockObjectStore = mock(ObjectStore.class);
     OzoneVolume mockVolume = mock(OzoneVolume.class);
@@ -266,10 +279,23 @@ private void 
assertUnhandledOMExceptionPropagated(OMException omException,
         .build();
     endpoint.queryParamsForTest().set(S3Consts.QueryParams.LIFECYCLE, "");
 
-    OS3Exception ex = assertThrows(OS3Exception.class,
-        () -> endpoint.put("bucket1", onePrefix()));
-    assertEquals(expectedHttpCode, ex.getHttpCode());
-    assertEquals(expectedErrorCode, ex.getCode());
+    return assertThrows(OS3Exception.class, () -> endpoint.put("bucket1", 
onePrefix()));
+  }
+
+  @Test
+  public void 
testPutLifecycleConfigurationWithPastExpirationDateReturnsDetailedMessage() 
throws Exception {
+    OMException omException = assertThrows(OMException.class,
+        () -> new 
OmLCExpiration.Builder().setDate("2020-01-01T00:00:00Z").build()
+            .valid(System.currentTimeMillis()));
+    assertTrue(omException.getMessage().contains("must be in the future"),
+        "Precondition: OM's real validation message should explain the date is 
in the past, got: "
+            + omException.getMessage());
+
+    OS3Exception ex = putLifecycleConfigurationThrowingOm(omException);
+    assertEquals(HTTP_BAD_REQUEST, ex.getHttpCode());
+    assertEquals(INVALID_REQUEST.getCode(), ex.getCode());
+    assertEquals(omException.getMessage(), ex.getErrorMessage(),
+        "The client should receive OM's detailed message explaining the date 
is in the past");
   }
 
   @Test


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

Reply via email to