使用Retrofit 2向Spring Webflux后端多文件上传报错排查
问题分析与解决方案
核心错误原因
org.springframework.core.codec.DecodingException: Could not find first boundary 错误的本质是:Android端上传的multipart请求体内容无效,服务器无法识别分隔边界。对比Postman和Retrofit的请求头可以发现,Retrofit请求的content-length仅为528,远小于Postman的1555045,说明上传的文件内容根本没有被正确读取。
问题出在Android读取照片的代码中:从相册选择的文件Uri(如content://media/external/images/media/xxx)的path字段并非真实文件系统路径,直接用File(uri.path!!)创建的文件对象实际不存在,导致上传的是空内容或无效数据。
修复步骤
1. 修改Android端文件读取逻辑
不再直接将Uri转为File,而是保留Uri用于后续读取文件流:
if (activityResult.resultCode == Activity.RESULT_OK) { val photoUris = mutableListOf<Uri>() // 处理多选情况 activityResult.data?.clipData?.let { clipData -> for (i in 0 until clipData.itemCount) { photoUris.add(clipData.getItemAt(i).uri) } } ?: run { // 处理单选情况 activityResult.data?.data?.let { uri -> photoUris.add(uri) } } loveSpotPhotoService.uploadToLoveSpot(loveSpotId, photoUris, this@LoveSpotDetailsActivity) }
2. 修改Retrofit客户端上传逻辑
通过ContentResolver从Uri读取文件流,生成有效的RequestBody:
suspend fun uploadToLoveSpot(loveSpotId: Long, photoUris: List<Uri>, activity: Activity) { val loadingBarShower = LoadingBarShower(activity).show() withContext(Dispatchers.IO) { val contentResolver = activity.contentResolver val parts: List<MultipartBody.Part> = photoUris.mapIndexed { index, uri -> // 获取文件原始名称 val fileName = getFileName(contentResolver, uri) ?: "photo_$index.jpg" // 基于文件流创建RequestBody val requestFile = object : RequestBody() { override fun contentType(): MediaType? { return MediaType.parse(contentResolver.getType(uri) ?: "image/*") } override fun writeTo(sink: BufferedSink) { contentResolver.openInputStream(uri)?.use { inputStream -> sink.writeAll(inputStream.source()) } ?: throw IOException("无法打开文件输入流") } } MultipartBody.Part.createFormData("photos", fileName, requestFile) } 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) } } } // 辅助方法:从Uri获取文件显示名称 private fun getFileName(contentResolver: ContentResolver, uri: Uri): String? { contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { val nameColumnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameColumnIndex != -1) { return cursor.getString(nameColumnIndex) } } } return null }
3. 额外检查点
- 权限确认:Android 10及以上版本,确保应用拥有
READ_MEDIA_IMAGES权限(Android 13+)或使用存储访问框架(SAF);Android 9及以下需申请READ_EXTERNAL_STORAGE权限。 - 字段一致性:确认Retrofit中
createFormData("photos", ...)的字段名与服务器@RequestPart("photos")完全一致。
内容的提问来源于stack exchange,提问作者LordScone
相关产品推荐
相关产品推荐

