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

turcsanyi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 75184260be1 NIFI-15685 - ListS3 V1 pagination fails when delimiter is 
not set causing infinite loop for buckets with more than 1000 objects
75184260be1 is described below

commit 75184260be137931c198690dcef32a971581c940
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Mar 9 12:41:33 2026 +0100

    NIFI-15685 - ListS3 V1 pagination fails when delimiter is not set causing 
infinite loop for buckets with more than 1000 objects
    
    This closes #10983.
    
    Signed-off-by: Peter Turcsanyi <[email protected]>
---
 .../org/apache/nifi/processors/aws/s3/ListS3.java  | 10 ++-
 .../apache/nifi/processors/aws/s3/TestListS3.java  | 92 ++++++++++++++++++++++
 2 files changed, 101 insertions(+), 1 deletion(-)

diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java
index 3f6ec56f15b..b81aac8822b 100644
--- 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java
@@ -900,7 +900,15 @@ public class ListS3 extends AbstractS3Processor implements 
VerifiableProcessor {
 
         @Override
         public void setNextMarker() {
-            listObjectsRequestBuilder.marker(listObjectsResponse.nextMarker());
+            final String nextMarker = listObjectsResponse.nextMarker();
+            if (nextMarker != null) {
+                listObjectsRequestBuilder.marker(nextMarker);
+            } else {
+                final List<S3Object> contents = listObjectsResponse.contents();
+                if (!contents.isEmpty()) {
+                    
listObjectsRequestBuilder.marker(contents.get(contents.size() - 1).key());
+                }
+            }
         }
 
         @Override
diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestListS3.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestListS3.java
index 93fde82b0c4..6160c19db20 100644
--- 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestListS3.java
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestListS3.java
@@ -849,6 +849,98 @@ public class TestListS3 {
         
when(mockS3Client.listObjects(any(ListObjectsRequest.class))).thenReturn(response);
     }
 
+    @Test
+    public void testListV1PaginationWithoutDelimiter() {
+        final List<String> firstPageKeys = List.of("key-0001", "key-0002", 
"key-0003");
+        final List<String> secondPageKeys = List.of("key-0004", "key-0005", 
"key-0006");
+        setupV1PaginationMocks(firstPageKeys, null, secondPageKeys, null);
+
+        runner.run();
+
+        final List<ListObjectsRequest> requests = 
captureListObjectsRequests(2);
+        assertNull(requests.get(0).marker());
+        assertEquals("key-0003", requests.get(1).marker());
+
+        runner.assertAllFlowFilesTransferred(ListS3.REL_SUCCESS, 6);
+    }
+
+    @Test
+    public void testListV1PaginationWithDelimiter() {
+        runner.setProperty(ListS3.DELIMITER, "/");
+
+        final List<String> firstPageKeys = List.of("key-0001", "key-0002");
+        final List<String> secondPageKeys = List.of("key-0003", "key-0004");
+        setupV1PaginationMocks(firstPageKeys, "key-0002", secondPageKeys, 
null);
+
+        runner.run();
+
+        final List<ListObjectsRequest> requests = 
captureListObjectsRequests(2);
+        assertNull(requests.get(0).marker());
+        assertEquals("key-0002", requests.get(1).marker());
+
+        runner.assertAllFlowFilesTransferred(ListS3.REL_SUCCESS, 4);
+    }
+
+    @Test
+    public void testListV1PaginationWithEmptyContentsAndNoNextMarker() {
+        final ListObjectsResponse firstPageResponse = 
ListObjectsResponse.builder()
+                .contents(Collections.emptyList())
+                .isTruncated(true)
+                .build();
+
+        final List<String> secondPageKeys = List.of("key-0001", "key-0002");
+        final ListObjectsResponse secondPageResponse = 
buildListObjectsResponse(secondPageKeys, false, null);
+
+        when(mockS3Client.listObjects(any(ListObjectsRequest.class)))
+                .thenReturn(firstPageResponse)
+                .thenReturn(secondPageResponse);
+
+        runner.setProperty(RegionUtil.REGION, "eu-west-1");
+        runner.setProperty(ListS3.BUCKET_WITHOUT_DEFAULT_VALUE, "test-bucket");
+        runner.setProperty(ListS3.LIST_TYPE, "1");
+
+        runner.run();
+
+        final List<ListObjectsRequest> requests = 
captureListObjectsRequests(2);
+        assertNull(requests.get(0).marker());
+        assertNull(requests.get(1).marker());
+
+        runner.assertAllFlowFilesTransferred(ListS3.REL_SUCCESS, 2);
+    }
+
+    private void setupV1PaginationMocks(final List<String> firstPageKeys, 
final String firstPageNextMarker,
+                                        final List<String> secondPageKeys, 
final String secondPageNextMarker) {
+        runner.setProperty(RegionUtil.REGION, "eu-west-1");
+        runner.setProperty(ListS3.BUCKET_WITHOUT_DEFAULT_VALUE, "test-bucket");
+        runner.setProperty(ListS3.LIST_TYPE, "1");
+
+        final ListObjectsResponse firstPageResponse = 
buildListObjectsResponse(firstPageKeys, true, firstPageNextMarker);
+        final ListObjectsResponse secondPageResponse = 
buildListObjectsResponse(secondPageKeys, false, secondPageNextMarker);
+
+        when(mockS3Client.listObjects(any(ListObjectsRequest.class)))
+                .thenReturn(firstPageResponse)
+                .thenReturn(secondPageResponse);
+    }
+
+    private ListObjectsResponse buildListObjectsResponse(final List<String> 
keys, final boolean isTruncated, final String nextMarker) {
+        final Instant lastModified = Instant.now();
+        final List<S3Object> objects = keys.stream()
+                .map(key -> 
S3Object.builder().key(key).lastModified(lastModified).build())
+                .toList();
+
+        return ListObjectsResponse.builder()
+                .contents(objects)
+                .isTruncated(isTruncated)
+                .nextMarker(nextMarker)
+                .build();
+    }
+
+    private List<ListObjectsRequest> captureListObjectsRequests(final int 
expectedCount) {
+        final ArgumentCaptor<ListObjectsRequest> captureRequest = 
ArgumentCaptor.forClass(ListObjectsRequest.class);
+        verify(mockS3Client, 
Mockito.times(expectedCount)).listObjects(captureRequest.capture());
+        return captureRequest.getAllValues();
+    }
+
     @Test
     void testMigration() {
         final PropertyMigrationResult propertyMigrationResult = 
runner.migrateProperties();

Reply via email to