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

Kotlin向AWS预签名URL二进制传图遇403错误排查

问题描述

尝试通过流将图片上传至S3预签名URL时返回403错误,但该文件通过POSTMAN可正常上传,使用Retrofit却失败,请问遗漏了什么?

用户代码实现

val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)//bitmap is the bitmap of the image
val byteArray = stream.toByteArray()
val encodedImage = Base64.encodeToString(byteArray, Base64.DEFAULT)
val requestBody : RequestBody = encodedImage.toRequestBody()// for base64 format              

val url = "https://s3.presigned_url.com"
val baseUrl = url.split(".com").toTypedArray()[0] + ".com"
var queryUrl: String? = url.split(".com").toTypedArray()[1]

val client = OkHttpClient.Builder().build()

val retrofit_image_upload: Retrofit = Retrofit.Builder()
    .baseUrl(baseUrl)
    .addConverterFactory(GsonConverterFactory.create())
    .client(client)
    .build()

var request1 = retrofit_image_upload.create(Api::class.java)
try {
    request1.upload(
        url,
        fileSize,//size of the image
        requestBody,
    ).enqueue(
        object : retrofit2.Callback<Void> {
            override fun onResponse(call: Call<Void>, response: Response<Void>) {
                println(response.code().toString())
            }
            override fun onFailure(call: Call<Void>, t: Throwable) {
                println(t.toString())
            }
        }
    )
} catch (e: IOException) {
    e.printStackTrace()
}

Retrofit上传接口

@PUT
fun upload(
    @Url url: String,
    @Header("content-length") contentlength: Int,
    @Body image: RequestBody,
): Call<Void>

用户尝试将请求体转为流后问题仍存在,参考相关文章未解决。


问题分析与解决方案

出现403的核心原因是请求体格式不匹配、预签名URL使用错误,以下是具体修正点:

  • 移除Base64编码:S3预签名PUT请求要求上传原始二进制文件,你将Bitmap转成Base64字符串后,请求体内容与Postman上传的原始文件不一致,导致签名校验失败。直接用原始字节数组创建RequestBody并指定正确MIME类型:

    val requestBody = byteArray.toRequestBody("image/jpeg".toMediaTypeOrNull())
    
  • 修正预签名URL使用方式:Retrofit的@Url参数可直接传入完整预签名URL,无需拆分baseUrl和路径。拆分操作会破坏预签名URL中的签名参数,导致请求无效。创建Retrofit时使用任意合法的S3 baseUrl即可:

    val retrofit_image_upload: Retrofit = Retrofit.Builder()
        .baseUrl("https://s3.amazonaws.com/")
        .client(client)
        .build()
    
  • 删除手动设置的Content-Length:OkHttp会自动计算并添加正确的Content-Length头部,手动设置可能与实际请求体长度不匹配,触发签名校验失败。直接移除接口中的@Header("content-length")参数。

  • 移除GsonConverterFactory:上传二进制文件不需要Gson转换器,添加后会干扰请求体处理,导致数据格式错误。

修正后的完整代码

实现代码

val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
val byteArray = stream.toByteArray()
// 直接用原始字节数组创建RequestBody,指定MIME类型
val requestBody = byteArray.toRequestBody("image/jpeg".toMediaTypeOrNull())

val presignedUrl = "https://s3.presigned_url.com"

val client = OkHttpClient.Builder().build()

val retrofit_image_upload: Retrofit = Retrofit.Builder()
    .baseUrl("https://s3.amazonaws.com/") // 通用S3 baseUrl
    .client(client)
    .build()

val request1 = retrofit_image_upload.create(Api::class.java)
request1.upload(presignedUrl, requestBody).enqueue(object : retrofit2.Callback<Void> {
    override fun onResponse(call: Call<Void>, response: Response<Void>) {
        println("响应码:${response.code()}")
    }
    override fun onFailure(call: Call<Void>, t: Throwable) {
        println("请求失败:${t.message}")
    }
})

Retrofit接口

@PUT
fun upload(
    @Url url: String,
    @Body image: RequestBody
): Call<Void>

内容的提问来源于stack exchange,提问作者Lokesh Bisht

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 04:35:36