Skip to content

Commit

Permalink
Allow non-ascii header values on Android & add utf-8 fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
robertying committed Mar 20, 2023
1 parent 7b916be commit 5cf6090
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 10 deletions.
4 changes: 3 additions & 1 deletion packages/react-native/Libraries/Network/FormData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,35 @@

package com.facebook.react.modules.network;

import java.lang.reflect.Method;
import okhttp3.Headers;

/**
* The class purpose is to weaken too strict OkHttp restriction on http headers. See:
* https://github.com/square/okhttp/issues/2016 Auth headers might have an Authentication
* information. It is better to get 401 from the server in this case, rather than non descriptive
* error as 401 could be handled to invalidate the wrong token in the client code.
* The class purpose is to provide compatibility among OkHttp versions on adding non-ascii header values.
*
* For v3.12.0 or higher, we can use the `addUnsafeAscii` method to add non-ascii header values.
* See: https://square.github.io/okhttp/changelogs/changelog_3x/#version-3120
* We need to use reflection to call this method, as it is not available in older versions.
* Remove reflection once the internal version of OkHttp is updated to v3.12.0 or higher.
*
* For other versions, we need to strip non-ascii header values.
* See: https://github.com/square/okhttp/issues/2016
* Auth headers might have an Authentication information. It is better to get 401 from the server
* in this case, rather than non descriptive error as 401 could be handled to invalidate the wrong
* token in the client code.
*/
public class HeaderUtil {

public static Method addUnsafeNonAsciiMethod = null;

static {
try {
addUnsafeNonAsciiMethod = Headers.Builder.class.getMethod("addUnsafeNonAscii", String.class, String.class);
} catch (NoSuchMethodException e) {
// Ignore
}
}

public static String stripHeaderName(String name) {
StringBuilder builder = new StringBuilder(name.length());
boolean modified = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import com.facebook.react.module.annotations.ReactModule;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
Expand Down Expand Up @@ -760,11 +762,24 @@ public void removeListeners(double count) {}
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);

if (HeaderUtil.addUnsafeNonAsciiMethod != null) {
try {
// Use reflection to call addUnsafeNonAscii because it's not available in
// older versions of OkHttp that are used internally.
HeaderUtil.addUnsafeNonAsciiMethod.invoke(headersBuilder, headerName, headerValue);
} catch (IllegalAccessException | InvocationTargetException e) {
// Stripping non-ascii characters is needed for the regular `add` method.
headersBuilder.add(headerName, HeaderUtil.stripHeaderValue(headerValue));
}
} else {
// Stripping non-ascii characters is needed for the regular `add` method.
headersBuilder.add(headerName, HeaderUtil.stripHeaderValue(headerValue));
}
}
if (headersBuilder.get(USER_AGENT_HEADER_NAME) == null && mDefaultUserAgent != null) {
headersBuilder.add(USER_AGENT_HEADER_NAME, mDefaultUserAgent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.StandardCharsets;
import com.facebook.react.common.network.OkHttpCallUtil;
import com.facebook.react.modules.network.HeaderUtil;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -502,7 +503,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
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(
Expand Down Expand Up @@ -538,7 +539,17 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
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");

if (HeaderUtil.addUnsafeNonAsciiMethod != null) {
// We're on a version of OkHttp that supports non-ascii header values
// so we should expect the non-ascii characters to be preserved.
assertThat(bodyHeaders.get(1).get("content-disposition")).isEqualTo("filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg");
} else {
// We're on a version of OkHttp that doesn't support non-ascii header values
// so we should expect the non-ascii characters to be stripped.
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);
}
Expand Down

0 comments on commit 5cf6090

Please sign in to comment.