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:
Code Overview:
S3UploadService — synchronous, uses RequestBody.fromBytes(...).
S3AsyncUploadService — asynchronous, uses AsyncRequestBody.fromBytes(...).
An endpoint /s3/upload-string uploads a simple string to both services:
Observed Behavior:
sync-uploaded.txt contains the expected content.
async-uploaded.txt is present in the bucket, but has 0 bytes.
Expected:
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?
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.
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));
}