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

Android端使用Retrofit2调用已生成AWS预签名URL上传MP4视频失败求助

预签名URL直传S3 Broken pipe报错修复方案

问题根因拆解

你遇到的断连报错本质是S3服务端主动拒绝了请求,并非手机端SSL本身的问题,核心触发点有三个:

  • 自定义RequestBody的contentLength()方法存在缺陷,部分场景下返回-1,触发OkHttp自动使用Transfer-Encoding: chunked分块编码传输,而S3预签名PUT接口完全不支持分块编码,收到请求会直接断开TCP连接,最终在手机端写入数据时抛出Broken pipe错误。
  • 用Multipart封装文件上传的逻辑不符合S3预签名接口要求:预签名PUT接口要求请求体是纯文件二进制内容,Multipart格式会额外添加分隔符、表单字段头信息,导致S3校验签名时和预签名生成的计算值不匹配,直接拒绝连接。
  • 请求头和预签名生成时的配置不一致:手动添加的Content-Type、User-Agent头部和生成预签名时传入的头部参数不匹配,触发签名校验失败断连。

可直接落地的修复步骤

  • 重写RequestBody实现,强制准确读取文件大小,从根源避免Chunked传输
    替换之前依赖MediaStore Cursor查询大小的逻辑,改用文件描述符直接读取真实大小,彻底杜绝返回-1的可能:
    // 适配任意uri的文件大小读取扩展
    fun ContentResolver.getRealFileSize(uri: Uri): Long {
        // 优先通过文件描述符读取真实大小,解决MediaStore Cursor查询不准的问题
        val fdSize = openFileDescriptor(uri, "r")?.use { it.statSize }
        if (fdSize != null && fdSize > 0) return fdSize
        // 本地文件路径兜底
        if (uri.scheme == ContentResolver.SCHEME_FILE) return File(uri.path!!).length()
        // 读不到大小直接抛异常,禁止触发分块传输
        throw IllegalStateException("无法准确获取文件大小,已终止上传逻辑避免触发S3断连")
    }
    
    // 适配S3上传的RequestBody
    fun ContentResolver.buildS3RequestBody(uri: Uri, mediaType: MediaType): RequestBody {
        val realSize = getRealFileSize(uri)
        return object : RequestBody() {
            // 直接返回文件正确的Content-Type
            override fun contentType() = mediaType
            // 强制返回准确的文件大小,不做返回-1的兜底
            override fun contentLength() = realSize
            override fun writeTo(sink: BufferedSink) {
                openInputStream(uri)?.source()?.use { inputSource ->
                    sink.writeAll(inputSource)
                } ?: throw IOException("无法打开文件输入流")
            }
        }
    }
    
  • 修正Retrofit接口定义,移除多余的头部参数
    不要手动通过@Header添加Content-Type、User-Agent这类和签名计算相关的头部,头部配置完全由RequestBody和预签名URL本身携带的信息决定:
    interface FileUploadService {
        @PUT
        fun uploadToPresignedUrl(
            @Url presignedUrl: String,
            @Body fileBody: RequestBody
        ): Call<Void>
    }
    
  • 简化Retrofit初始化配置
    因为上传的是原始二进制流,不需要Gson转换器参与解析,去掉冗余配置减少干扰:
    val retrofit = Retrofit.Builder()
        // 预签名是完整绝对地址,baseUrl填合法格式即可,会被@Url的全地址覆盖
        .baseUrl("https://placeholder.example.com/")
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .build()
    
  • 移除所有Multipart上传相关逻辑
    预签名PUT接口不兼容Multipart格式的封装结构,直接把纯二进制的RequestBody传入接口即可,不需要做表单项封装。

常见踩坑提醒

  • 绝对不能让contentLength()返回-1:一旦返回-1,OkHttp会自动携带Transfer-Encoding: chunked请求头,S3端收到这类请求会直接断连,这是Broken pipe报错的最常见诱因。
  • 请求头必须和预签名生成时的配置完全一致:如果生成预签名时指定了Content-Type: video/mp4,那请求里的Content-Type必须完全匹配,不能有大小写差异、也不能多带其他自定义头部,否则签名校验不通过会被直接拒绝。
  • 不要随意修改预签名URL的路径、参数,必须完整使用生成时返回的原始URL做请求地址。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.28 03:15:04