You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 07:55:32