阿里云对象存储(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 头部,用于描述字段的属性,如 namefilename 等。下面是一个 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 构造表单上传文件,对应源代码在下面文件路径:

https://github.com/facebook/react-native/blame/main/packages/react-native/Libraries/Network/FormData.js

可以找到 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)} 删除即可。