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

通过Uri向Retrofit(S3签名URL)上传图片失败问题排查

解决S3签名URL上传时的411 Length Required错误

从你的OkHttp日志能直接定位问题:请求返回了411 Length Required错误,对应错误码是MissingContentLength——这是因为你的InputStreamRequestBody里contentLength()方法返回了-1,导致OkHttp没有发送Content-Length请求头,而DigitalOcean Spaces(和AWS S3逻辑一致)要求PUT上传必须提供这个头部。

核心修复:正确计算并返回内容长度

你需要修改InputStreamRequestBody,从ContentResolver中获取Uri对应文件的实际长度,而不是返回-1。这里提供两种可靠的获取方式:

class InputStreamRequestBody(
    private val contentType: MediaType,
    private val contentResolver: ContentResolver,
    private val uri: Uri
) : RequestBody() {
    override fun contentType() = contentType

    override fun contentLength(): Long {
        // 优先通过FileDescriptor获取文件大小,效率更高
        contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
            return descriptor.statSize
        }
        
        // 如果FileDescriptor方式失败,尝试通过ContentResolver查询
        val cursor = contentResolver.query(
            uri,
            arrayOf(MediaStore.MediaColumns.SIZE),
            null,
            null,
            null
        )
        
        return try {
            if (cursor?.moveToFirst() == true) {
                cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE))
            } else {
                // 万不得已才返回-1,尽量避免,否则还是会触发411错误
                throw IOException("无法获取文件长度")
            }
        } finally {
            cursor?.close()
        }
    }
    
    @Throws(IOException::class)
    override fun writeTo(sink: BufferedSink) {
        val input = contentResolver.openInputStream(uri)
        input?.use { sink.writeAll(it.source()) } ?: throw IOException("Could not open $uri")
    }
}

额外需要注意的细节

  1. Content-Type一致性:
    看你的日志,签名URL里的Content-Type是image/webp,但你创建InputStreamRequestBody时用的是image/jpeg,这会导致签名验证不匹配,即使上传成功也可能出现文件类型错误。建议直接从Uri获取实际MIME类型:

    val actualType = contentResolver.getType(uri)?.toMediaType() ?: "image/jpeg".toMediaType()
    val contentPart = InputStreamRequestBody(actualType, resolver, content)
    
  2. 权限检查:
    如果你的Uri来自系统相册或其他外部存储,确保App有对应的读取权限(Android 10+用分区存储模式下,大部分场景不需要显式申请READ_EXTERNAL_STORAGE,但旧设备或特殊Uri场景仍需确认)。

为什么之前的代码会失败?

当contentLength()返回-1时,OkHttp会使用**分块传输编码(Chunked Transfer Encoding)**发送请求,但S3兼容的存储服务通常不支持PUT请求使用分块编码,必须明确指定Content-Length才能完成上传。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 21:12:40