Use existing character set in POST body when possible (#23603)

Summary:
This commit fixes a bug introduced in a previous attempt (https://github.com/facebook/react-native/pull/23580) to address an issue where okhttp appended `charset=utf-8` to the Content-Type header when otherwise not specified.

In that commit, I converted all characters to UTF-8, however it should instead use an existing encoding when possible.

Related issues:
https://github.com/facebook/react-native/issues/8237#issuecomment-466304854

[Android][fixed] - Respect existing character set when specified in fetch() POST request
Pull Request resolved: https://github.com/facebook/react-native/pull/23603

Differential Revision: D14191750

Pulled By: hramos

fbshipit-source-id: 11c1bfd98ccd33cd8e54ea426285b7d2ce9c2d7c
This commit is contained in:
Nate Hunzaker 2019-02-22 16:10:11 -08:00 committed by Mike Grabowski
parent 4cad737315
commit d7c4c37f09
2 changed files with 96 additions and 1 deletions

View File

@ -372,7 +372,13 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
return;
}
} else {
requestBody = RequestBody.create(contentMediaType, body.getBytes(StandardCharsets.UTF_8));
// Use getBytes() to convert the body into a byte[], preventing okhttp from
// appending the character set to the Content-Type header when otherwise unspecified
// https://github.com/facebook/react-native/issues/8237
Charset charset = contentMediaType == null
? StandardCharsets.UTF_8
: contentMediaType.charset(StandardCharsets.UTF_8);
requestBody = RequestBody.create(contentMediaType, body.getBytes(charset));
}
} else if (data.hasKey(REQUEST_BODY_KEY_BASE64)) {
if (contentType == null) {

View File

@ -18,6 +18,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.StandardCharsets;
import com.facebook.react.common.network.OkHttpCallUtil;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
@ -346,6 +347,94 @@ public class NetworkingModuleTest {
assertThat(argumentCaptor.getValue().body().contentType().toString()).isEqualTo("application/json");
}
@Test
public void testRespectsExistingCharacterSet() throws Exception {
RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class);
ReactApplicationContext context = mock(ReactApplicationContext.class);
when(context.getJSModule(any(Class.class))).thenReturn(emitter);
OkHttpClient httpClient = mock(OkHttpClient.class);
when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Call callMock = mock(Call.class);
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
JavaOnlyMap body = new JavaOnlyMap();
body.putString("string", "Friðjónsson");
mockEvents();
networkingModule.sendRequest(
"POST",
"http://somedomain/bar",
0,
JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain; charset=utf-16")),
body,
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0,
/* withCredentials */ false);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
Buffer contentBuffer = new Buffer();
argumentCaptor.getValue().body().writeTo(contentBuffer);
assertThat(contentBuffer.readString(StandardCharsets.UTF_16)).isEqualTo("Friðjónsson");
}
@Test
public void testGracefullyRecoversFromInvalidContentType() throws Exception {
RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class);
ReactApplicationContext context = mock(ReactApplicationContext.class);
when(context.getJSModule(any(Class.class))).thenReturn(emitter);
OkHttpClient httpClient = mock(OkHttpClient.class);
when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Call callMock = mock(Call.class);
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
JavaOnlyMap body = new JavaOnlyMap();
body.putString("string", "test");
mockEvents();
networkingModule.sendRequest(
"POST",
"http://somedomain/bar",
0,
JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "invalid")),
body,
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0,
/* withCredentials */ false);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
Buffer contentBuffer = new Buffer();
argumentCaptor.getValue().body().writeTo(contentBuffer);
assertThat(contentBuffer.readString(StandardCharsets.UTF_8)).isEqualTo("test");
assertThat(argumentCaptor.getValue().header("Content-Type")).isEqualTo("invalid");
}
@Test
public void testMultipartPostRequestSimple() throws Exception {
PowerMockito.mockStatic(RequestBodyUtil.class);