Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] Allow non-ascii header values & add utf-8 filename fallback #35060

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,7 @@
import com.facebook.react.module.annotations.ReactModule;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
Expand Down Expand Up @@ -760,11 +761,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