<!-- Love localstack? Please consider supporting our collective:
👉  https://opencollective.com/localstack/donate -->

Found a pagination bug with localstack's s3. Primarily against s3Client.listNextBatchOfObjects.

    private void test() {
        s3Client.putObject(s3BucketName, "key1", "content");
        s3Client.putObject(s3BucketName, "key2", "content");
        s3Client.putObject(s3BucketName, "key3", "content");

        ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
                .withBucketName(s3BucketName)
                .withPrefix("")
                .withDelimiter("/")
                .setMaxKeys(1); // 1 Key per request

        ObjectListing objectListing = s3Client.listObjects(listObjectsRequest);
        List<SomeObject> someObjList = createEmptyList();
        someObjList.addAll(mapFilesToSomeObject(objectListing)); // puts at least 1 item into the list

        while (objectListing.isTruncated()) {
            objectListing = s3Client.listNextBatchOfObjects(objectListing);
            someObjList.addAll(mapFilesToSomeObject(objectListing));
        }
        assertEquals(3, someObjList.size());
    }

    private List<SomeObject> mapFilesToSomeObject(ObjectListing objectListing) {
        return objectListing.getObjectSummaries()
            .stream()
            .map(S3ObjectSummary::getKey)
            .map(x -> this.toSomeObject(x)) // some deserialization method
            .collect(Collectors.toList());
    }

This while loop will only get the second item in its first loop. It still enters the loop once more to grab the third item since the condition is still true, but the 2nd call of listNextBatchOfObjects in that very loop returns an empty list, thus failing to grab that third item. I confirmed that listNextBatchOfObjects retrieves the "second page" only against localstack, with changing the amount of items and page sizes. On AWS s3, this works as expected.

Refer to s3Client.listNextBatchOfObjects:
https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3Client.html#listNextBatchOfObjects-com.amazonaws.services.s3.model.ObjectListing-
from localstack.config import HOSTNAME, HOSTNAME_EXTERNAL
from localstack.utils import persistence
from localstack.utils.aws import aws_stack
from localstack.utils.common import (
    short_uid, timestamp, TIMESTAMP_FORMAT_MILLIS, to_str, to_bytes, clone, md5, get_service_protocol)
from localstack.utils.analytics import event_publisher
from localstack.utils.aws.aws_responses import requests_response
from localstack.services.s3 import multipart_content
from localstack.services.generic_proxy import ProxyListener
S3_NOTIFICATIONS = {}
BUCKET_CORS = {}
BUCKET_LIFECYCLE = {}
BUCKET_REPLICATIONS = {}
BUCKET_ENCRYPTIONS = {}
OBJECT_LOCK_CONFIGS = {}
LOGGER = logging.getLogger(__name__)
XMLNS_S3 = 'http://s3.amazonaws.com/doc/2006-03-01/'
BUCKET_NAME_REGEX = (r'(?=^.{3,63}$)(?!^(\d+\.)+\d+$)' +
    r'(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$)')
NOTIFICATION_DESTINATION_TYPES = ('Queue', 'Topic', 'CloudFunction', 'LambdaFunction')
OBJECT_METADATA_KEY_PREFIX = 'x-amz-meta-'
ALLOWED_HEADER_OVERRIDES = {
    'response-content-type': 'Content-Type',
    'response-content-language': 'Content-Language',
    'response-expires': 'Expires',
    'response-cache-control': 'Cache-Control',
    'response-content-disposition': 'Content-Disposition',
    'response-content-encoding': 'Content-Encoding',
}
def event_type_matches(events, action, api_method):