阿里云对象存储(OSS) 在 React Native 0.74 中的兼容问题
问题描述
更新 React Native 0.74 之后通过 multipart/form-data
直传到阿里云 OSS 会返回状态码 400 和以下错误信息:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>FieldItemTooLong</Code>
<Message>Your name of form field is too long.</Message>
<RequestId>6654***</RequestId>
<HostId>***.oss-cn-shanghai.aliyuncs.com</HostId>
<MaxSizeAllowed>8192</MaxSizeAllowed>
<ProposedSize>522742</ProposedSize>
<EC>0006-00000110</EC>
<RecommendDoc>https://api.aliyun.com/troubleshoot?q=0006-00000110</RecommendDoc>
</Error>
根据错误信息检查了全部参数,确定不太可能是参数过长,猜测是阿里云没有按标准解析请求体错误导致的问题。对应接口文档 https://help.aliyun.com/zh/oss/developer-reference/postobject 。
问题分析
我们知道对于 multipart/form-data
类型的请求,通过 boundary
分割表单字段,每个字段都有一个 Content-Disposition
头部,用于描述字段的属性,如 name
、filename
等。下面是一个 Content-Type: multipart/form-data; boundary=_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns
对应的请求体:
--_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns
content-disposition: form-data; name="key"
***/IMG_1449.png
--_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns
content-disposition: form-data; name="OSSAccessKeyId"
***
--_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns
content-disposition: form-data; name="policy"
eyJle***
--_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns
content-disposition: form-data; name="signature"
oLN9N***
--_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns
content-disposition: form-data; name="callback"
eyJjY***
--_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns
content-disposition: form-data; name="file"; filename="呵呵.png"; filename*=utf-8''%E5%91%B5%E5%91%B5.png
content-type: image/png
PNG...
--_QRVTWFxfqR7nwWRAy3OY9tC.Q6doMj356S9UWHhdtfUYfwdnwRf8Zj5KFVFCyZTArkUns--
经过构造各种请求测试,最后发现是 filename="呵呵.png"; filename*=utf-8''%E5%91%B5%E5%91%B5.png
导致的问题。当然这里不是中文的问题,即使是 filename="1.png"; filename*=utf-8''1.png
也会导致错误。可以推测这里阿里云 OSS 没有按照 RFC5987 标准解析请求体,导致解析错误。
为什么 Chrome 不会有问题?下面是 Chrome 的请求体,可以看到即使是中文也不会使用 filename*=utf-8''
格式所以不会有问题。
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLoOVxlROy1xVy4V0
------WebKitFormBoundaryLoOVxlROy1xVy4V0
Content-Disposition: form-data; name="key"
***.csv
------WebKitFormBoundaryLoOVxlROy1xVy4V0
Content-Disposition: form-data; name="OSSAccessKeyId"
***
------WebKitFormBoundaryLoOVxlROy1xVy4V0
Content-Disposition: form-data; name="policy"
eyJle***
------WebKitFormBoundaryLoOVxlROy1xVy4V0
Content-Disposition: form-data; name="signature"
0qqrY***
------WebKitFormBoundaryLoOVxlROy1xVy4V0
Content-Disposition: form-data; name="callback"
eyJjYW***
------WebKitFormBoundaryLoOVxlROy1xVy4V0
Content-Disposition: form-data; name="file"; filename="流量合作-广告位列表.csv"
Content-Type: text/csv
------WebKitFormBoundaryLoOVxlROy1xVy4V0--
问题来源
确定问题之后可以进一步找到 React Native 对应源代码,使用 FormData
构造表单上传文件,对应源代码在下面文件路径:
可以找到 filename*=utf-8''
的处理逻辑如下:
getParts(): Array<FormDataPart> {
return this._parts.map(([name, value]) => {
const contentDisposition = 'form-data; name="' + name + '"';
const headers: Headers = {'content-disposition': contentDisposition};
// The body part is a "blob", which in React Native just means
// an object with a `uri` attribute. Optionally, it can also
// have a `name` and `type` attribute to specify filename and
// content type (cf. web Blob interface.)
if (typeof value === 'object' && !Array.isArray(value) && value) {
if (typeof value.name === 'string') {
headers['content-disposition'] += `; filename="${
value.name
}"; filename*=utf-8''${encodeURI(value.name)}`;
}
if (typeof value.type === 'string') {
headers['content-type'] = value.type;
}
return {...value, headers, fieldName: name};
}
// Convert non-object values to strings as per FormData.append() spec
return {string: String(value), headers, fieldName: name};
});
}
这是在 PR#35060 引入的代码,这个代码本身没有问题,问题出在阿里云 OSS 的服务端。
临时解决方案
在阿里云 OSS 修复前,可以加一个补丁将 node_modules/react-native/Libraries/Network/FormData.js
中的 ; filename*=utf-8''${encodeURI(value.name)}
删除即可。