diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 0bfeb3f1ef4491..209ead5a396872 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -372,7 +372,13 @@ public void onProgress(long bytesWritten, long contentLength, boolean done) { 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) { diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java index cb6136e7f07404..cdbcb8908c057f 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java @@ -18,6 +18,7 @@ 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 Object answer(InvocationOnMock invocation) throws Throwable { 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() { + @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 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() { + @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 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);