React Native通过S3预签名URL上传图片损坏或显示白块问题排查
React Native 上传图片到S3预签名URL文件损坏问题排查与解决方案
所有上传后文件损坏、仅显示小白块的核心原因,都是没有将真实的图片二进制内容正确传递给S3:要么传了路径字符串、元数据对象,要么传了未解码的base64文本、带冗余分隔符的FormData内容,和file URI前缀、Blob方法本身没有本质关系。
各失败方案的具体问题
- 初始axios传
file.uri:实际传递的是file:///xxx.jpg这段路径字符串,不是图片内容,修改URI前缀完全无效,因为根本没有读取文件本身。 - 直接传选图返回的file对象、FormData:前者是选图库返回的元信息对象,不是二进制文件;后者是multipart格式,会自动拼接boundary分隔符,但生成预签名URL时没有校验这类格式头,S3会把分隔符计入文件内容直接导致损坏。
fetch(file.uri).blob()报错:是旧版React Native的fetch实现对本地文件URI的支持缺陷,不是blob方案逻辑错误。- 传base64显示小白块:直接把base64字符串塞进请求体,还错误添加
Content-Encoding: base64配置,S3不会自动解码base64内容,存进去的是编码后的文本,自然无法正常显示。 - 用axios上传:React Native环境下axios无法正确识别本地文件URI,会自动将传入参数序列化为JSON字符串,无论怎么调整配置都容易出内容错误。
可直接落地的修复方案
优先选第一种二进制上传方案,比base64省30%左右流量,兼容性最好。
方案1:原生XHR直接上传本地二进制(无需额外依赖)
后端预签名URL逻辑不需要修改,保持原有传ContentType参数的逻辑即可,不要加ContentEncoding配置,不要把ContentType改成application/octet-stream,保证前端上传时的Content-Type和预签名时传入的类型完全一致即可。
前端上传代码直接替换为以下内容:
/** * 上传图片到S3预签名URL * @param {string} preSignedUrl 后端返回的预签名PUT地址 * @param {object} file react-native-image-picker返回的assets[0]对象 */ export const uploadImageToS3 = (preSignedUrl, file) => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState !== 4) return if (xhr.status === 200) return resolve('上传成功') return reject(new Error(`上传失败,状态码:${xhr.status}`)) } xhr.open('PUT', preSignedUrl) // 只设置Content-Type头,不要加其他自定义头、鉴权头,否则会导致预签名校验失败 xhr.setRequestHeader('Content-Type', file.type) // 重点:只传带uri属性的对象,不要加name、type等其他属性,不要包装FormData xhr.send({ uri: file.uri }) }) }
注意:iOS端默认返回的
file:///前缀URI不需要修改,安卓端如果返回content://开头的URI,只要在react-native-image-picker的配置里固定mediaType: 'photo'即可直接使用,不需要做路径转换。
方案2:base64兼容上传(适配低版本安卓机型)
如果方案1在部分安卓低版本机型上有兼容问题,再使用base64方案,注意必须将base64解码为原始二进制再传递:
- 后端生成预签名URL的逻辑去掉
ContentEncoding: 'base64'配置,ContentType保持和图片实际类型一致。 - 前端上传代码:
import RNFS from 'react-native-fs' export const uploadBase64ImageToS3 = async (preSignedUrl, file) => { // 读取文件base64内容 const base64Str = await RNFS.readFile(file.uri, 'base64') // 将base64解码为原始二进制ArrayBuffer const binaryStr = atob(base64Str) const byteLength = binaryStr.length const byteArray = new Uint8Array(byteLength) for (let i = 0; i < byteLength; i++) { byteArray[i] = binaryStr.charCodeAt(i) } return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState !== 4) return if (xhr.status === 200) return resolve('上传成功') return reject(new Error(`上传失败,状态码:${xhr.status}`)) } xhr.open('PUT', preSignedUrl) xhr.setRequestHeader('Content-Type', file.type) // 传递解码后的二进制数据,不要直接传base64字符串 xhr.send(byteArray.buffer) }) }
问题排查步骤
如果按上述代码修改后仍有异常,按顺序校验:
- 先验证预签名URL有效性:拿到预签名URL后,用Postman选PUT请求,Body选binary模式,选本地一张同类型图片上传,如果上传后能正常打开,说明后端逻辑完全正常,问题仅出在前端。
- 上传完成后去S3控制台下载文件,对比本地原文件大小:如果S3上的文件大小远小于本地文件(比如本地2M,S3上只有几十/几百字节),说明还是传错了内容,没有传递正确的二进制。
- 检查是否有全局请求拦截器自动给上传请求加了额外头(比如
Authorization、X-Requested-With等),S3预签名URL如果携带了签名时未包含的头,会直接校验失败或导致内容异常,上传S3时不要用封装过的带全局拦截器的请求实例。 - 不要用FormData传PUT请求到普通预签名URL,FormData仅适用于POST方式的分段上传场景,普通PUT上传直接传二进制即可。
内容的提问来源于stack exchange,提问作者user2465134
相关产品推荐
相关产品推荐

