如何使用Retrofit发送文件?Retrofit2传图遇ENOENT错误求助
Retrofit2上传图片出现ENOENT错误的排查与修复
错误open failed: ENOENT (No such file or directory)的核心原因就是指定路径的文件不存在,先从路径问题入手排查,再修正代码里的细节问题。
一、优先排查文件路径问题
- 先打印
userAvatar的具体值,确认路径格式是否正确:是绝对路径还是相对路径?如果是从相册/相机获取的Uri转来的路径,Android 10+的Scoped Storage机制下,直接用File访问私有目录外的文件会失败。 - 加个文件存在性判断,提前定位问题:
val file = File(userAvatar) if (!file.exists()) { Log.e("FILE_ERROR", "文件不存在: $userAvatar") message.value = "头像文件找不到" return@launch } - 检查权限:如果是访问外部存储的图片,Android 13+需要申请
READ_MEDIA_IMAGES权限,低于13版本需要READ_EXTERNAL_STORAGE权限,没权限也会导致文件无法访问。
二、代码本身的问题点
1. Retrofit接口的冗余配置
你当前的接口注解有错误:
@Headers( "accept: */*", "Content-Type: multipart/form-data" ) @Multipart @POST("avatar") suspend fun updateAvatar(@Header("Authorization") auth: String, @Part("file") userImage: RequestBody): Response<JsonObject>
@Multipart注解会自动生成带boundary的Content-Type: multipart/form-data头,手动添加的Content-Type会覆盖这个自动配置,导致服务器无法解析多部分请求,必须删掉这个Header。- 上传文件更规范的写法是用
MultipartBody.Part,可以带上文件名,避免服务器无法识别文件类型:@Multipart @POST("avatar") suspend fun updateAvatar( @Header("Authorization") auth: String, @Part file: MultipartBody.Part ): Response<JsonObject>
2. 上传逻辑的细节优化
- 你现在是先更新用户资料,成功后再传头像,这个逻辑如果符合业务需求没问题,但要注意如果资料更新失败,头像就不会上传,是否是预期行为?
userToken.replace("\"", "")这个操作要确认token本身是否真的带引号,避免误操作导致token失效。
三、修复后的完整代码示例
修正后的Retrofit接口
@Multipart @POST("avatar") suspend fun updateAvatar( @Header("Authorization") auth: String, @Part file: MultipartBody.Part ): Response<JsonObject>
修正后的ViewModel逻辑
fun updateProfile( firstName: String, patronymic: String, lastName: String, birthday: String, gender: String, userAvatar: String, userToken: String ) { // 重置状态变量 message.value = null sendEmailResponseCode.value = null checkEmailResponseCode.value = null createCardResponseCode.value = null viewModelScope.launch { val apiService = ApiService.getInstance() try { val profileResponse = apiService.updateProfile( "Bearer ${userToken.replace("\"", "")}", mapOf( "firstname" to firstName, "lastname" to lastName, "patronymic" to patronymic, "birthday" to birthday, "gender" to gender ) ) // 先校验文件是否存在 val avatarFile = File(userAvatar) if (!avatarFile.exists()) { message.value = "头像文件不存在: $userAvatar" createCardResponseCode.value = profileResponse.code() return@launch } if (profileResponse.isSuccessful) { // 构建Multipart文件部分 val requestBody = avatarFile.asRequestBody("image/*".toMediaType()) val avatarPart = MultipartBody.Part.createFormData("file", avatarFile.name, requestBody) val avatarResponse = apiService.updateAvatar("Bearer ${userToken.replace("\"", "")}", avatarPart) Log.d("LOG", "头像上传结果: $avatarResponse") } else if (profileResponse.code() == 403) { message.value = "无权限,请重新登录" } else { message.value = "资料更新失败,错误码: ${profileResponse.code()}" } createCardResponseCode.value = profileResponse.code() } catch (e: Exception) { Log.e("ERROR", "更新流程出错", e) message.value = "更新失败: ${e.message}" } } }
四、Android平台额外注意事项
如果userAvatar是Content Uri(比如相册返回的content://开头的路径),不能直接用File访问,需要通过ContentResolver读取流来构建RequestBody:
val contentResolver = context.contentResolver val inputStream = contentResolver.openInputStream(Uri.parse(userAvatar)) ?: throw IOException("无法打开文件流") val requestBody = RequestBody.create("image/*".toMediaType(), inputStream.readBytes()) val avatarPart = MultipartBody.Part.createFormData("file", "avatar.jpg", requestBody)
内容的提问来源于stack exchange,提问作者TriangleN
相关产品推荐
相关产品推荐

