From f791fb9e660fe15bccf55029045c48f4bbcbc5cb Mon Sep 17 00:00:00 2001 From: Hugo FOYART <11079152+foyarash@users.noreply.github.com> Date: Mon, 2 Dec 2024 03:14:28 -0800 Subject: [PATCH] fix: FormData filename in content-disposition (#46543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This Pull Request fixes a regression introduced in https://github.com/facebook/react-native/commit/7c7e9e6571c1f702213e9ffbb40921cd5a1a786b, which adds a `filename*` attribute to the `content-disposition` of a FormData part. However, as the [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#directives) states, there is no `filename*` attribute for the `content-disposition` header in case of a form data. The `filename*` attribute would break the parsing of form data in the request, such as in frameworks like `Next.js` which uses the web implementation of [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request). Fixes https://github.com/facebook/react-native/issues/44737 ## Changelog: [General] [Fixed] - Remove non compliant `filename*` attribute in a FormData `content-disposition` header Pull Request resolved: https://github.com/facebook/react-native/pull/46543 Test Plan: - Clone the `react-native` repo - Create a simple JS file that will act as a node server and execute it ```javascript const http = require('http'); const server = http.createServer(async function (r, res) { const req = new Request(new URL(r.url, 'http://localhost:3000'), { headers: r.headers, method: r.method, body: r, duplex: 'half', }); const fileData = await req.formData(); console.log(fileData); res.writeHead(200); res.end(); }); server.listen(3000); ``` - Go to `packages/rn-tester` - Add a `useEffect` in `js/RNTesterAppShared.js` ```javascript React.useEffect(() => { const formData = new FormData(); formData.append('file', { uri: 'https://www.gravatar.com/avatar', name: '测试photo/1.jpg', type: 'image/jpeg', }); fetch('http://localhost:3000', { method: 'POST', body: formData, }).then(res => console.log(res.ok)); }); ``` - Run the app on iOS or Android - The node server should output the file added to the FormData with an encoded name Reviewed By: robhogan Differential Revision: D66643317 Pulled By: yungsters fbshipit-source-id: 0d531528005025bff303505363671e854c0a2b63 --- .../react-native/Libraries/Network/FormData.js | 14 +++++++++++--- .../Libraries/Network/__tests__/FormData-test.js | 5 ++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/react-native/Libraries/Network/FormData.js b/packages/react-native/Libraries/Network/FormData.js index 91735c6b033..b237b45ae24 100644 --- a/packages/react-native/Libraries/Network/FormData.js +++ b/packages/react-native/Libraries/Network/FormData.js @@ -28,6 +28,15 @@ type FormDataPart = ... }; +/** + * Encode a FormData filename compliant with RFC 2183 + * + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#directives + */ +function encodeFilename(filename: string): string { + return encodeURIComponent(filename.replace(/\//g, '_')); +} + /** * Polyfill for XMLHttpRequest2 FormData API, allowing multipart POST requests * with mixed data (string, native files) to be submitted via XMLHttpRequest. @@ -82,9 +91,8 @@ class FormData { // 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)}`; + headers['content-disposition'] += + `; filename="${encodeFilename(value.name)}"`; } if (typeof value.type === 'string') { headers['content-type'] = value.type; diff --git a/packages/react-native/Libraries/Network/__tests__/FormData-test.js b/packages/react-native/Libraries/Network/__tests__/FormData-test.js index ee741afb7c9..18830d5f15f 100644 --- a/packages/react-native/Libraries/Network/__tests__/FormData-test.js +++ b/packages/react-native/Libraries/Network/__tests__/FormData-test.js @@ -48,8 +48,7 @@ describe('FormData', function () { type: 'image/jpeg', name: 'photo.jpg', headers: { - 'content-disposition': - 'form-data; name="photo"; filename="photo.jpg"; filename*=utf-8\'\'photo.jpg', + 'content-disposition': 'form-data; name="photo"; filename="photo.jpg"', 'content-type': 'image/jpeg', }, fieldName: 'photo', @@ -70,7 +69,7 @@ describe('FormData', function () { name: '测试photo.jpg', headers: { 'content-disposition': - 'form-data; name="photo"; filename="测试photo.jpg"; filename*=utf-8\'\'%E6%B5%8B%E8%AF%95photo.jpg', + 'form-data; name="photo"; filename="%E6%B5%8B%E8%AF%95photo.jpg"', 'content-type': 'image/jpeg', }, fieldName: 'photo',