0

S3AsyncUploadService.java

package com.util.s3;

import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@Component
public class S3AsyncUploadService {

    private final S3AsyncClient s3AsyncClient;

    public S3AsyncUploadService(S3AsyncClient s3AsyncClient) {
        this.s3AsyncClient = s3AsyncClient;
    }

    public Mono<Void> uploadFile(String bucketName, String fileName, byte[] fileContent) {
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .build();

        return Mono.fromFuture(
                s3AsyncClient.putObject(
                        putObjectRequest,
                        AsyncRequestBody.fromBytes(fileContent)
                )
        ).then();
    }
}

S3UploadService.java

package com.util.s3;

import org.springframework.stereotype.Component;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@Component
public class S3UploadService {

    private final S3Client s3Client;

    public S3UploadService(S3Client s3Client) {
        this.s3Client = s3Client;
    }

    public void uploadFile(String bucketName, String fileName, byte[] fileContent) {
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .build();

        s3Client.putObject(putObjectRequest, RequestBody.fromBytes(fileContent));
    }
}

S3Resource.java

package com.util.resource;

import com.util.s3.S3AsyncUploadService;
import com.util.s3.S3UploadService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
import software.amazon.awssdk.services.s3.model.S3Object;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/s3")
public class S3Resource {

    private final S3UploadService s3UploadService;
    private final S3AsyncUploadService s3AsyncUploadService;
    private final S3Client s3Client;

    @Value("${aws.s3.bucket}")
    private String bucketName;

    public S3Resource(S3UploadService s3UploadService, S3AsyncUploadService s3AsyncUploadService, S3Client s3Client) {
        this.s3UploadService = s3UploadService;
        this.s3AsyncUploadService = s3AsyncUploadService;
        this.s3Client = s3Client;
    }

    @GetMapping("/upload-string")
    public Mono<ResponseEntity<Map<String, Long>>> uploadString(@RequestParam("content") String content) {
        s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
        String syncKey = "sync-uploaded.txt";
        String asyncKey = "async-uploaded.txt";
        byte[] bytes = content.getBytes(StandardCharsets.UTF_8);

        // 1. Synchronous upload
        s3UploadService.uploadFile(bucketName, syncKey, bytes);

        // 2. Asynchronous upload
        return s3AsyncUploadService.uploadFile(bucketName, asyncKey, bytes)
                .then(Mono.fromCallable(() -> {
                    ListObjectsV2Request listReq = ListObjectsV2Request.builder()
                            .bucket(bucketName)
                            .build();
                    ListObjectsV2Response listRes = s3Client.listObjectsV2(listReq);
                    Map<String, Long> keysWithSize = listRes.contents().stream()
                            .collect(Collectors.toMap(S3Object::key, S3Object::size));
                    return ResponseEntity.ok(keysWithSize);
                }));
    }
}

Problem:

I'm using Java 21 with Spring Boot WebFlux and AWS SDK v2. I have two services to upload files to S3:

  1. S3UploadService uses the synchronous S3Client — works as expected.
  2. S3AsyncUploadService uses the asynchronous S3AsyncClient — creates the S3 object (key), but the file has 0 bytes, even though the content is not empty.

Code Overview:

  • S3UploadService — synchronous, uses RequestBody.fromBytes(...).

  • S3AsyncUploadService — asynchronous, uses AsyncRequestBody.fromBytes(...).

  • An endpoint /s3/upload-string uploads a simple string to both services:

    • Creates a bucket.
    • Uploads one file using the sync service and another using the async service.
    • Lists the objects and returns the object sizes.
    • The sync-uploaded file has the expected size. The async-uploaded file has size 0.

Observed Behavior:

  • sync-uploaded.txt contains the expected content.

  • async-uploaded.txt is present in the bucket, but has 0 bytes.

Expected:

  • Both uploads should store the same content (the same byte[] is used).

What I’ve Tried:

  • Confirmed the input byte array is not empty.

  • Ensured .then() is chained after the Mono.fromFuture(...).

  • Verified the bucket and object keys are correct.

Question: What might be causing the S3AsyncClient to upload a 0-byte file, even though the content is present?

3
  • Can you debug here? ListObjectsV2Response listRes = s3Client.listObjectsV2(listReq); Commented Jul 28 at 11:06
  • Try using public Mono<S3AsyncClient> instead of public Mono<Void> , and use flatMap() instead of then() in the endpoint. Commented Jul 28 at 11:16
  • @0x52 I've tried that... still seeing the same issue. Source code for reference: github.com/reap-sow/aws-sdk-s3-test Commented Jul 28 at 18:54

1 Answer 1

0

Try this, it's a little simpler.

@Component
public class S3AsyncUploadService {

    private final S3AsyncClient s3AsyncClient;

    public S3AsyncUploadService(S3AsyncClient s3AsyncClient) {
        this.s3AsyncClient = s3AsyncClient;
    }

    public Mono<PutObjectResponse> uploadFile(String bucketName, String fileName, byte[] fileContent) {

        final PutObjectRequest request = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .build();

        return Mono.fromFuture(s3AsyncClient.putObject(request, AsyncRequestBody.fromBytes(fileContent)))
                .doOnError(e -> System.err.println("Error uploading file: " + e.getMessage()))
                .doOnSuccess(response -> System.out.println("Upload completed"));
    }
}
@GetMapping(value = "/test")
public Mono<String> uploadString(@RequestParam("content") String content) {
    String bucketName = "test-async-rubn";
    String keyName = "test/reactive-test.txt";

    return s3AsyncUploadService.uploadFile(bucketName, keyName, content.getBytes(StandardCharsets.UTF_8))
            .thenReturn("Content uploaded successfully");
}

I have removed ListObjectsV2Request because you are also merging it with the synchronous client, and I don't see the point.

enter image description here

enter image description here

enter image description here

I have also added the other way to list the files and size of each one, as you were doing above.

public Mono<Map<String, Long>> uploadFile(String bucketName, String fileName, byte[] fileContent) {

        final PutObjectRequest request = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .build();

       
        return Mono.fromFuture(s3AsyncClient.putObject(request, AsyncRequestBody.fromBytes(fileContent)))
                .doOnError(e -> System.err.println("Error uploading file: " + e.getMessage()))
                .doOnSuccess(response -> System.out.println("Upload completed"))
                .then(getFromFuture(bucketName));

}

private Mono<Map<String, Long>> getFromFuture(String bucketName) {
        return Mono.fromFuture(() -> {

            ListObjectsV2Request listReq = ListObjectsV2Request.builder()
                    .bucket(bucketName)
                    .build();

            return s3AsyncClient.listObjectsV2(listReq)
                    .thenApply((e) -> {
                        return e.contents().stream()
                                .collect(Collectors.toMap(S3Object::key, S3Object::size));
                    });
        });
}
@GetMapping(value = "/test")
public Mono<Map<String, Long>> uploadString(@RequestParam("content") String content) {
    String bucketName = "test-async-rubn";
    String keyName = "test/reactive-test.txt";

    return s3AsyncUploadService.uploadFile(bucketName, keyName, content.getBytes(StandardCharsets.UTF_8));
}

enter image description here

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks! I think the underlying issue was the S3 Mock I was using. Unfortunately, I couldn't find any other lightweight local S3 Mock framework.
On my website, you can contact me, and I can lend you my bucket. In any case, I think that the synchronous client, together with your reactive service, was the problem in the mix I had.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.