Multipart Upload on S3 with Java Spring Boot and IO Streams

Elvis Ciotti
2 min readSep 2, 2021

Spring endpoint accepting multipart JSON + Uploaded file

To accept a multipart composed by a file and also JSON content, you can define the endpoint with two @RequestPart with the name name of the part (“json”, and “file” for the actual uploaded file). Keep those two names in mind as they’ll be needed later.
The json part is your POJO (MyJsonContentRequest) with the request data.
The uploaded file is the Spring MultipartFile class that has method to get the original file name, MIME and of course the uploaded file input stream.

import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.bind.annotation.*;
...@PostMapping("")
@Consumes(MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public MyResponse createAsset(
@Valid @RequestPart("json") MyJsonContentRequest jsonPart,
@Valid @NotNull @RequestPart("file") MultipartFile file
) {
...
}

Upload into S3 without keeping the whole file in memory

Most of the SDK example from Amazon require to consume the stream in order to calculate file hash (needed in S3), but it seems currently possible to upload skipping the file hash, and sending the stream directly to S3. That might vary depending on the AWS version, so please test it.

import com.amazonaws.services.s3.AmazonS3;...// create metadata with only content type and size, that you can both take from the uploaded Multipart
final ObjectMetadata metaData = new ObjectMetadata();
metaData.setContentType(contentType);
metaData.setContentLength(size);
// create and call S3 request to create the new S3 object
PutObjectRequest putObjectRequest = new PutObjectRequest(
bucketName,
objectKey, // file/object name in S3
streamToUpload, // input stream from the Multipart
metaData // created above, with the only content type and size
);

amazonS3.putObject(putObjectRequest);

In case you want to send the MD5 of the file, you can consume the stream and calculate it.

import com.amazonaws.util.IOUtils;final byte[] objectBytes = IOUtils.toByteArray(streamToUpload);
metaData.setContentMD5(calculateObjectContentMD5(objectBytes));
metaData.setContentLength(objectBytes.length);
streamToUpload = new ByteArrayInputStream(objectBytes);

where calculateObjectContentMD5 is defined as

@SneakyThrows
public static String calculateObjectContentMD5(byte[] objectBytes) {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
messageDigest.update(objectBytes);

return new String(Base64.encodeBase64(messageDigest.digest()));
}

Javascript code

Create a FormData object and append the two parts (named exactly after the Spring endpoint parts)

formData = new FormData();

formData.append("file", document.forms[form1].file.files[0]);
formData.append('json', new Blob([JSON.stringify({
"a": "b",
"c": "d"
})], {
type: "application/json"
}));

You can send this content both via fetch or axios to our POST endpoint, where the formData is the data to submit.

Important: Make sure you do NOT set a Content-Type for the request, as the content type is defined individually for each part. In case of problems, debug the actual HTTP content begin sent (Chrome network tab), you should see something like this. Javascript libs tends to require or set a Content-Type, so make sure it’s not specified, so that the browser auto-adds the right one as soon as it detects there are two parts. If you are unsure, debug the request in the browser and check that the Content-Type is set to multipart/form-data (see below)

Accept:application/json, */*
Content-Type:multipart/form-data; boundary=----
RANDOM_STRING

------RANDOM_STRING
Content-Disposition: form-data; name="file"; filename="file.jpg"
Content-Type: image/jpeg <--- (MIME of the file)


------RANDOM_STRING
Content-Disposition: form-data; name="json"; filename="json"
Content-Type: application/json


------RANDOM_STRING--

Clap if this helped you.

Comment for corrections

--

--

Elvis Ciotti

Software Contractor — Java, Spring, k8s, AWS, Javascript @ London - hire me at https://www.linkedin.com/in/elvisciotti