使用OkHttp上传至S3预签名URL时SSL异常问题排查
你的问题核心在于两种HTTP客户端对请求内容长度的处理逻辑不同,进而导致与Amazon S3的兼容性差异,触发了SSL连接重置错误。
1. 核心差异:分块编码(Chunked Transfer Encoding)的使用
OkHttp的行为
你实现的UriRequestBody中,contentLength()直接返回-1L,这会告诉OkHttp:无法提前知晓请求体的长度。此时OkHttp会采用Transfer-Encoding: chunked的方式上传数据——把文件拆分成多个小块(chunk)逐个发送,每个块附带自己的长度标识。
而Amazon S3对于预签名的PUT请求,在处理1MiB以上大文件的分块编码上传时存在兼容性问题:S3服务器可能会因为分块传输的方式,在SSL层触发连接重置(Connection reset by peer),尤其是当预签名URL生成时未明确指定Content-Length参数的情况下。
HttpURLConnection的行为
你的HttpURLConnection实现中,虽然没有手动设置Content-Length,但它会自动做适配处理:
- 当从
ContentResolver获取文件输入流时,HttpURLConnection会间接读取文件的元数据(比如通过ContentResolver的文件描述符),自动获取文件真实长度并设置Content-Length头,使用固定长度传输而非分块编码。 - 即使HttpURLConnection触发了分块编码,它的分块逻辑与OkHttp不同,恰好能兼容S3的服务器要求。
2. 其他可能的差异细节
除了分块编码,还有两个细节可能影响结果:
- IO缓冲策略:OkHttp依赖Okio库进行IO操作,其缓冲逻辑与Java原生的
InputStream.copyTo(OutputStream)不同,可能导致写入速率或数据块大小不符合S3的预期,间接触发SSL层的连接重置。 - 默认请求头差异:OkHttp会自动添加一些默认请求头(比如
Accept-Encoding),而HttpURLConnection的默认请求头更简洁,这些细微差异也可能导致S3服务器的处理逻辑不同。
3. 修复方案
解决问题的关键是让OkHttp使用固定长度传输,而非分块编码,只需修改你的UriRequestBody:
class UriRequestBody( private val file: Uri, private val contentResolver: ContentResolver, private val mediaType: MediaType = MediaType.parse("application/octet-stream")!! ) : RequestBody() { // 从ContentResolver获取文件真实长度 override fun contentLength(): Long { return contentResolver.openFileDescriptor(file, "r")?.use { it.statSize } ?: -1L } override fun contentType(): MediaType? = mediaType override fun writeTo(sink: BufferedSink) { Okio.source(contentResolver.openInputStream(file)).use { sink.writeAll(it) } } }
通过返回真实文件长度,OkHttp会自动设置Content-Length头,和HttpURLConnection的行为保持一致,从而避免S3的SSL连接重置问题。
另外,你也可以优化预签名URL的生成逻辑:在生成S3预签名PUT URL时,明确指定ContentLength参数,让S3服务器更严格地验证请求完整性,进一步减少兼容性问题:
const signedUrl = s3.getSignedUrl('putObject', { Bucket: config.bucket.name, Key: `${fileName}`, Expires: signedUrlExpireSeconds, ContentLength: fileSize // 传入文件的真实长度 });
内容的提问来源于stack exchange,提问作者shelll

