mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Allow non-ascii header values & add utf-8 filename fallback (#35060)
Summary: Fix https://github.com/facebook/react-native/issues/31537: [Android] React Native strips non-ASCII characters from HTTP headers ## Changelog <!-- Help reviewers and the release process by writing your own changelog entry. For an example, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [Android] [Changed] - Allow non-ascii header values on Android and add utf-8 filename fallback in FormData Pull Request resolved: https://github.com/facebook/react-native/pull/35060 Test Plan: 1. Clone the `react-native` repo. 2. Build the rn-tester app. 3. Prepare tests 1. Add `android:usesCleartextTraffic="true"` to AndroidManifest.xml 2. Use the following code as a server: ```javascript const http = require('http'); const requestListener = function (req, res) { // raw header value console.log(req.headers['content-disposition']); // nodejs assumes the header value is ISO-8859-1 encoded console.log(Buffer.from(req.headers['content-disposition'], 'latin1').toString('utf-8')); // decode encoded header value if it's sent as UTF-8 console.log(decodeURI(req.headers['content-disposition'])); res.writeHead(200); res.end(); }; const server = http.createServer(requestListener); server.listen(3000); ``` 3. Run `adb reverse tcp:3000 tcp:3000` to connect the 3000 port on the emulator if necessary. 4. Edit `RNTesterAppShared.js` to include test code: ```javascript useEffect(() => { fetch('http://localhost:3000/', { headers: { 'Content-Type': 'multipart/form-data; charset=utf-8', 'Content-Disposition': `attachment; filename*=utf-8''${encodeURI( 'filename测试abc.jpg', )}`, }, }).then(res => { console.log(res.ok); }); fetch('http://localhost:3000/', { headers: { 'Content-Type': 'multipart/form-data; charset=utf-8', 'Content-Disposition': `attachment; filename="filename测试abc.jpg"`, }, }).then(res => { console.log(res.ok); }); }, []); ``` 5. Both requests should succeed; without the fix, the second request received by the server will not have the utf-8 characters "测试" in the header value. Reviewed By: NickGerleman Differential Revision: D40639985 Pulled By: cortinico fbshipit-source-id: 005f2481976046a92a26239ad704780ac58d4a44
This commit is contained in:
committed by
Facebook GitHub Bot
parent
8b2f324a9b
commit
7c7e9e6571
+3
-1
@@ -82,7 +82,9 @@ 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 + '"';
|
||||
headers['content-disposition'] += `; filename="${
|
||||
value.name
|
||||
}"; filename*=utf-8''${encodeURI(value.name)}`;
|
||||
}
|
||||
if (typeof value.type === 'string') {
|
||||
headers['content-type'] = value.type;
|
||||
|
||||
@@ -48,7 +48,29 @@ describe('FormData', function () {
|
||||
type: 'image/jpeg',
|
||||
name: 'photo.jpg',
|
||||
headers: {
|
||||
'content-disposition': 'form-data; name="photo"; filename="photo.jpg"',
|
||||
'content-disposition':
|
||||
'form-data; name="photo"; filename="photo.jpg"; filename*=utf-8\'\'photo.jpg',
|
||||
'content-type': 'image/jpeg',
|
||||
},
|
||||
fieldName: 'photo',
|
||||
};
|
||||
expect(formData.getParts()[0]).toMatchObject(expectedPart);
|
||||
});
|
||||
|
||||
it('should return blob with the correct utf-8 handling', function () {
|
||||
formData.append('photo', {
|
||||
uri: 'arbitrary/path',
|
||||
type: 'image/jpeg',
|
||||
name: '测试photo.jpg',
|
||||
});
|
||||
|
||||
const expectedPart = {
|
||||
uri: 'arbitrary/path',
|
||||
type: 'image/jpeg',
|
||||
name: '测试photo.jpg',
|
||||
headers: {
|
||||
'content-disposition':
|
||||
'form-data; name="photo"; filename="测试photo.jpg"; filename*=utf-8\'\'%E6%B5%8B%E8%AF%95photo.jpg',
|
||||
'content-type': 'image/jpeg',
|
||||
},
|
||||
fieldName: 'photo',
|
||||
|
||||
-14
@@ -28,18 +28,4 @@ public class HeaderUtil {
|
||||
}
|
||||
return modified ? builder.toString() : name;
|
||||
}
|
||||
|
||||
public static String stripHeaderValue(String value) {
|
||||
StringBuilder builder = new StringBuilder(value.length());
|
||||
boolean modified = false;
|
||||
for (int i = 0, length = value.length(); i < length; i++) {
|
||||
char c = value.charAt(i);
|
||||
if ((c > '\u001f' && c < '\u007f') || c == '\t') {
|
||||
builder.append(c);
|
||||
} else {
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
return modified ? builder.toString() : value;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -761,11 +761,11 @@ public final class NetworkingModule extends NativeNetworkingAndroidSpec {
|
||||
return null;
|
||||
}
|
||||
String headerName = HeaderUtil.stripHeaderName(header.getString(0));
|
||||
String headerValue = HeaderUtil.stripHeaderValue(header.getString(1));
|
||||
String headerValue = header.getString(1);
|
||||
if (headerName == null || headerValue == null) {
|
||||
return null;
|
||||
}
|
||||
headersBuilder.add(headerName, headerValue);
|
||||
headersBuilder.addUnsafeNonAscii(headerName, headerValue);
|
||||
}
|
||||
if (headersBuilder.get(USER_AGENT_HEADER_NAME) == null && mDefaultUserAgent != null) {
|
||||
headersBuilder.add(USER_AGENT_HEADER_NAME, mDefaultUserAgent);
|
||||
|
||||
-25
@@ -27,46 +27,21 @@ class HeaderUtilTest {
|
||||
assertEquals(ALPHABET_TEST, HeaderUtil.stripHeaderName(ALPHABET_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueStripKeepsLetters() {
|
||||
assertEquals(ALPHABET_TEST, HeaderUtil.stripHeaderValue(ALPHABET_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nameStripKeepsNumbers() {
|
||||
assertEquals(NUMBERS_TEST, HeaderUtil.stripHeaderName(NUMBERS_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueStripKeepsNumbers() {
|
||||
assertEquals(NUMBERS_TEST, HeaderUtil.stripHeaderValue(NUMBERS_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueStripKeepsSpecials() {
|
||||
assertEquals(SPECIALS_TEST, HeaderUtil.stripHeaderValue(SPECIALS_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nameStripKeepsSpecials() {
|
||||
assertEquals(SPECIALS_TEST, HeaderUtil.stripHeaderName(SPECIALS_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueStripKeepsTabs() {
|
||||
assertEquals(TABULATION_TEST, HeaderUtil.stripHeaderValue(TABULATION_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nameStripDeletesTabs() {
|
||||
assertEquals(TABULATION_STRIP_EXPECTED, HeaderUtil.stripHeaderName(TABULATION_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueStripRemovesExtraSymbols() {
|
||||
assertEquals(BANNED_TEST_EXPECTED, HeaderUtil.stripHeaderValue(VALUE_BANNED_SYMBOLS_TEST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nameStripRemovesExtraSymbols() {
|
||||
assertEquals(BANNED_TEST_EXPECTED, HeaderUtil.stripHeaderName(NAME_BANNED_SYMBOLS_TEST))
|
||||
|
||||
+5
-2
@@ -482,7 +482,9 @@ public class NetworkingModuleTest {
|
||||
JavaOnlyArray.from(
|
||||
Arrays.asList(
|
||||
JavaOnlyArray.of("content-type", "image/jpg"),
|
||||
JavaOnlyArray.of("content-disposition", "filename=photo.jpg"))));
|
||||
JavaOnlyArray.of(
|
||||
"content-disposition",
|
||||
"filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg"))));
|
||||
formData.pushMap(imageBodyPart);
|
||||
|
||||
mNetworkingModule.sendRequest(
|
||||
@@ -521,7 +523,8 @@ public class NetworkingModuleTest {
|
||||
assertThat(bodyHeaders.get(0).get("content-disposition")).isEqualTo("user");
|
||||
assertThat(bodyRequestBody.get(0).contentType()).isNull();
|
||||
assertThat(bodyRequestBody.get(0).contentLength()).isEqualTo("locale".getBytes().length);
|
||||
assertThat(bodyHeaders.get(1).get("content-disposition")).isEqualTo("filename=photo.jpg");
|
||||
assertThat(bodyHeaders.get(1).get("content-disposition"))
|
||||
.isEqualTo("filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg");
|
||||
assertThat(bodyRequestBody.get(1).contentType()).isEqualTo(MediaType.parse("image/jpg"));
|
||||
assertThat(bodyRequestBody.get(1).contentLength()).isEqualTo("imageUri".getBytes().length);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user