通过Uri向Retrofit(S3签名URL)上传图片失败问题排查
从你的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") } }
额外需要注意的细节
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)权限检查:
如果你的Uri来自系统相册或其他外部存储,确保App有对应的读取权限(Android 10+用分区存储模式下,大部分场景不需要显式申请READ_EXTERNAL_STORAGE,但旧设备或特殊Uri场景仍需确认)。
为什么之前的代码会失败?
当contentLength()返回-1时,OkHttp会使用**分块传输编码(Chunked Transfer Encoding)**发送请求,但S3兼容的存储服务通常不支持PUT请求使用分块编码,必须明确指定Content-Length才能完成上传。
内容的提问来源于stack exchange,提问作者HavanaSun

