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();