I get the following error when I try to upload multiple files with retrofit 2:
org.springframework.core.codec.DecodingException: Could not find first boundary
When I upload multiple files with postman on the same API endpoint, it works perfectly.
Server controller endpoint:
@PostMapping("{loveSpotId}/photos")
suspend fun uploadToSpot(
@PathVariable loveSpotId: Long,
@RequestPart("photos") filePartFlux: Flux<FilePart>
) {
loveSpotPhotoService.uploadToSpot(loveSpotId, filePartFlux.asFlow())
}
Retrofit API definition:
interface LoveSpotPhotoApi {
@Multipart
@POST("/lovespots/{loveSpotId}/photos")
fun uploadToLoveSpot(
@Path("loveSpotId") loveSpotId: Long,
@Part photos: List<MultipartBody.Part>
): Call<ResponseBody>
// ...
}
Reading photos on Android device:
if (activityResult.resultCode == Activity.RESULT_OK) {
val itemCount: Int = activityResult.data?.clipData?.itemCount ?: 0
val files = ArrayList<File>()
for (i in 0 until itemCount) {
val clipData = activityResult.data!!.clipData!!
val uri = clipData.getItemAt(i).uri
files.add(File(uri.path!!))
}
loveSpotPhotoService.uploadToLoveSpot(loveSpotId, files, this@LoveSpotDetailsActivity)
}
Client code using Retrofit:
suspend fun uploadToLoveSpot(loveSpotId: Long, photos: List<File>, activity: Activity) {
val loadingBarShower = LoadingBarShower(activity).show()
withContext(Dispatchers.IO) {
val parts: List<MultipartBody.Part> = photos.map { prepareFilePart(it) }
val call = loveSpotPhotoApi.uploadToLoveSpot(loveSpotId, parts)
try {
val response = call.execute()
loadingBarShower.onResponse()
if (response.isSuccessful) {
toaster.showToast(R.string.photo_uploaded_succesfully)
} else {
toaster.showResponseError(response)
}
} catch (e: Exception) {
loadingBarShower.onResponse()
toaster.showToast(R.string.photo_upload_failed)
}
}
}
private fun prepareFilePart(file: File): MultipartBody.Part {
// create RequestBody instance from file
val requestFile = RequestBody.create(
MediaType.get("image/*"),
file
)
// MultipartBody.Part is used to send also the actual file name
return MultipartBody.Part.createFormData("photos", file.name, requestFile)
}
Example headers logged on server when I upload with postman and it works:
[Authorization:"Bearer ...", User-Agent:"PostmanRuntime/7.30.0", Accept:"/", Postman-Token:"7ad875eb-2fe5-40ea-99f0-3ad34c3fa875", Host:"localhost:8090", Accept-Encoding:"gzip, deflate, br", Connection:"keep-alive", Content-Type:"multipart/form-data; boundary=--------------------------877409032202838734061440", content-length:"1555045"]
Example headers logged on server when I upload with retrofit client and it fails:
[Authorization:"Bearer ...", Content-Type:"multipart/form-data; boundary=c6177139-6b31-4d91-b66d-54772a51d963", Host:"192.168.0.143:8090", Connection:"Keep-Alive", Accept-Encoding:"gzip", User-Agent:"okhttp/3.14.9", content-length:"528"]
The problem was that I was not reading photos from the Android device properly. Here is my code that fixed that:
launcher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
handlePhotoPickerResult(activityResult)
}
fun startPickerIntent(launcher: ActivityResultLauncher<Intent>) {
val intent = Intent()
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
intent.action = Intent.ACTION_PICK
launcher.launch(intent)
}
private fun handlePhotoPickerResult(activityResult: ActivityResult) {
MainScope().launch {
if (activityResult.resultCode == RESULT_OK) {
val files = PhotoUploadUtils.readResultToFiles(activityResult, contentResolver)
Log.i(this@LoveSpotDetailsActivity::class.simpleName, "Starting upload")
val result: Boolean = if (photoUploadReviewId != null) {
loveSpotPhotoService.uploadToReview(
loveSpotId,
photoUploadReviewId!!,
files,
this@LoveSpotDetailsActivity
)
} else {
loveSpotPhotoService.uploadToLoveSpot(
loveSpotId,
files,
this@LoveSpotDetailsActivity
)
}
photosLoaded = !result
photosRefreshed = !result
photoUploadReviewId = null
if (result) {
Log.i(
this@LoveSpotDetailsActivity::class.simpleName,
"Upload finished, starting refreshing views."
)
startPhotoRefreshSequence()
}
} else {
toaster.showToast(R.string.failed_to_access_photos)
}
}
}
fun readResultToFiles(
activityResult: ActivityResult,
contentResolver: ContentResolver
): List<File> {
val itemCount: Int = activityResult.data?.clipData?.itemCount ?: 0
val files = ArrayList<File>()
for (i in 0 until itemCount) {
val clipData = activityResult.data!!.clipData!!
val uri = clipData.getItemAt(i).uri
Log.i("uri", "$uri")
addToFilesFromUri(uri, files, contentResolver)
}
return files
}
private fun addToFilesFromUri(
uri: Uri,
files: ArrayList<File>,
contentResolver: ContentResolver
) {
val projection = arrayOf(MediaStore.MediaColumns.DATA)
contentResolver.query(uri, projection, null, null, null)
?.use { cursor ->
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA)
Log.i("columnIndex", "$columnIndex")
val filePath = cursor.getString(columnIndex)
Log.i("filePath", " $filePath")
if (filePath != null) {
val file = File(filePath)
Log.i("file", "$file")
files.add(file)
}
}
}
}
Rest of the code was fine.