Skip to content

Commit 9e436d1

Browse files
allengleyzerfacebook-github-bot
authored andcommitted
Added progress updates for all XMLHttpRequest upload types / fix crash on closed connection
Summary: This PR includes the same changes made in #16541, for addressing issues #11853/#15724. It adds upload progress updates for uploads with any request body type, and not just form-data. Additionally, this PR also includes a commit for fixing an `IllegalStateException` when a user's connection gets closed or times out (issues #10423/#11016). Since this exception was occurring within the progress updates logic, it started being thrown more frequently as a result of adding progress updates to all uploads, which was why the original PR was reverted. To test the upload progress updates, run the following JS to ensure events are now being dispatched: ``` const fileUri = 'file:///my_file.dat'; const url = 'http://my_post_url.com/'; const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (event) => { console.log('progress: ' + event.loaded + ' / ' + event.total); } xhr.onreadystatechange = () => {if (xhr.readyState === 4) console.log('done');} console.log('start'); xhr.open('POST', url); // sending a file (wasn't sending progress) xhr.setRequestHeader('Content-Type', 'image/jpeg'); xhr.send({ uri: fileUri }); // sending a string (wasn't sending progress) xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.send("some big string"); // sending form data (was already working) xhr.setRequestHeader('Content-Type', 'multipart/form-data'); const formData = new FormData(); formData.append('test', 'data'); xhr.send(formData); ``` To test the crash fix: In the RN Android project, before this change, set a breakpoint at `mRequestBody.writeTo(mBufferedSink);` of `ProgressRequestBody`, and wait a short while for a POST request with a non-null body to time out before resuming the app. Once resumed, if the connection was closed (the `closed` variable will be set to true in `RealBufferedSink`), an `IllegalStateException` will be thrown, which crashes the app. After the changes, an `IOException` will get thrown instead, which is already being properly handled. As mentioned above, includes the same changes as #16541, with an additional commit. [ANDROID] [BUGFIX] [XMLHttpRequest] - Added progress updates for all XMLHttpRequest upload types / fix crash on closed connection Previously, only form-data request bodies emitted upload progress updates. Now, other request body types will also emit updates. Also, Android will no longer crash on certain requests when user has a poor connection. Addresses issues: 11853/15724/10423/11016 Closes #17312 Differential Revision: D6712377 Pulled By: mdvacca fbshipit-source-id: bf5adc774703e7e66f7f16707600116f67201425
1 parent ef596de commit 9e436d1

File tree

4 files changed

+86
-59
lines changed

4 files changed

+86
-59
lines changed

ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ rn_android_library(
1111
],
1212
deps = [
1313
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"),
14+
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/internal:internal"),
1415
react_native_dep("third-party/java/infer-annotations:infer-annotations"),
1516
react_native_dep("third-party/java/jsr-305:jsr-305"),
1617
react_native_dep("third-party/java/okhttp:okhttp3"),

ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,11 @@ public void onProgress(long bytesWritten, long contentLength, boolean done) {
343343
}
344344
}
345345

346+
RequestBody requestBody;
346347
if (data == null) {
347-
requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method));
348+
requestBody = RequestBodyUtil.getEmptyBody(method);
348349
} else if (handler != null) {
349-
RequestBody requestBody = handler.toRequestBody(data, contentType);
350-
requestBuilder.method(method, requestBody);
350+
requestBody = handler.toRequestBody(data, contentType);
351351
} else if (data.hasKey(REQUEST_BODY_KEY_STRING)) {
352352
if (contentType == null) {
353353
ResponseUtil.onRequestError(
@@ -360,14 +360,13 @@ public void onProgress(long bytesWritten, long contentLength, boolean done) {
360360
String body = data.getString(REQUEST_BODY_KEY_STRING);
361361
MediaType contentMediaType = MediaType.parse(contentType);
362362
if (RequestBodyUtil.isGzipEncoding(contentEncoding)) {
363-
RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body);
363+
requestBody = RequestBodyUtil.createGzip(contentMediaType, body);
364364
if (requestBody == null) {
365365
ResponseUtil.onRequestError(eventEmitter, requestId, "Failed to gzip request body", null);
366366
return;
367367
}
368-
requestBuilder.method(method, requestBody);
369368
} else {
370-
requestBuilder.method(method, RequestBody.create(contentMediaType, body));
369+
requestBody = RequestBody.create(contentMediaType, body);
371370
}
372371
} else if (data.hasKey(REQUEST_BODY_KEY_BASE64)) {
373372
if (contentType == null) {
@@ -380,9 +379,7 @@ public void onProgress(long bytesWritten, long contentLength, boolean done) {
380379
}
381380
String base64String = data.getString(REQUEST_BODY_KEY_BASE64);
382381
MediaType contentMediaType = MediaType.parse(contentType);
383-
requestBuilder.method(
384-
method,
385-
RequestBody.create(contentMediaType, ByteString.decodeBase64(base64String)));
382+
requestBody = RequestBody.create(contentMediaType, ByteString.decodeBase64(base64String));
386383
} else if (data.hasKey(REQUEST_BODY_KEY_URI)) {
387384
if (contentType == null) {
388385
ResponseUtil.onRequestError(
@@ -403,9 +400,7 @@ public void onProgress(long bytesWritten, long contentLength, boolean done) {
403400
null);
404401
return;
405402
}
406-
requestBuilder.method(
407-
method,
408-
RequestBodyUtil.create(MediaType.parse(contentType), fileInputStream));
403+
requestBody = RequestBodyUtil.create(MediaType.parse(contentType), fileInputStream);
409404
} else if (data.hasKey(REQUEST_BODY_KEY_FORMDATA)) {
410405
if (contentType == null) {
411406
contentType = "multipart/form-data";
@@ -416,28 +411,16 @@ public void onProgress(long bytesWritten, long contentLength, boolean done) {
416411
if (multipartBuilder == null) {
417412
return;
418413
}
419-
420-
requestBuilder.method(
421-
method,
422-
RequestBodyUtil.createProgressRequest(
423-
multipartBuilder.build(),
424-
new ProgressListener() {
425-
long last = System.nanoTime();
426-
427-
@Override
428-
public void onProgress(long bytesWritten, long contentLength, boolean done) {
429-
long now = System.nanoTime();
430-
if (done || shouldDispatch(now, last)) {
431-
ResponseUtil.onDataSend(eventEmitter, requestId, bytesWritten, contentLength);
432-
last = now;
433-
}
434-
}
435-
}));
414+
requestBody = multipartBuilder.build();
436415
} else {
437416
// Nothing in data payload, at least nothing we could understand anyway.
438-
requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method));
417+
requestBody = RequestBodyUtil.getEmptyBody(method);
439418
}
440419

420+
requestBuilder.method(
421+
method,
422+
wrapRequestBodyWithProgressEmitter(requestBody, eventEmitter, requestId));
423+
441424
addRequest(requestId);
442425
client.newCall(requestBuilder.build()).enqueue(
443426
new Callback() {
@@ -515,6 +498,29 @@ public void onResponse(Call call, Response response) throws IOException {
515498
});
516499
}
517500

501+
private RequestBody wrapRequestBodyWithProgressEmitter(
502+
final RequestBody requestBody,
503+
final RCTDeviceEventEmitter eventEmitter,
504+
final int requestId) {
505+
if(requestBody == null) {
506+
return null;
507+
}
508+
return RequestBodyUtil.createProgressRequest(
509+
requestBody,
510+
new ProgressListener() {
511+
long last = System.nanoTime();
512+
513+
@Override
514+
public void onProgress(long bytesWritten, long contentLength, boolean done) {
515+
long now = System.nanoTime();
516+
if (done || shouldDispatch(now, last)) {
517+
ResponseUtil.onDataSend(eventEmitter, requestId, bytesWritten, contentLength);
518+
last = now;
519+
}
520+
}
521+
});
522+
}
523+
518524
private void readWithProgress(
519525
RCTDeviceEventEmitter eventEmitter,
520526
int requestId,

ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,75 @@
99

1010
package com.facebook.react.modules.network;
1111

12+
import com.facebook.common.internal.CountingOutputStream;
13+
1214
import java.io.IOException;
15+
1316
import okhttp3.MediaType;
1417
import okhttp3.RequestBody;
1518
import okio.BufferedSink;
16-
import okio.Buffer;
17-
import okio.Sink;
18-
import okio.ForwardingSink;
1919
import okio.Okio;
20+
import okio.Sink;
2021

2122
public class ProgressRequestBody extends RequestBody {
2223

2324
private final RequestBody mRequestBody;
2425
private final ProgressListener mProgressListener;
2526
private BufferedSink mBufferedSink;
27+
private long mContentLength = 0L;
2628

2729
public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) {
28-
mRequestBody = requestBody;
29-
mProgressListener = progressListener;
30+
mRequestBody = requestBody;
31+
mProgressListener = progressListener;
3032
}
3133

3234
@Override
3335
public MediaType contentType() {
34-
return mRequestBody.contentType();
36+
return mRequestBody.contentType();
3537
}
3638

3739
@Override
3840
public long contentLength() throws IOException {
39-
return mRequestBody.contentLength();
41+
if (mContentLength == 0) {
42+
mContentLength = mRequestBody.contentLength();
43+
}
44+
return mContentLength;
4045
}
4146

4247
@Override
4348
public void writeTo(BufferedSink sink) throws IOException {
44-
if (mBufferedSink == null) {
45-
mBufferedSink = Okio.buffer(sink(sink));
46-
}
47-
mRequestBody.writeTo(mBufferedSink);
48-
mBufferedSink.flush();
49+
if (mBufferedSink == null) {
50+
mBufferedSink = Okio.buffer(outputStreamSink(sink));
51+
}
52+
53+
// contentLength changes for input streams, since we're using inputStream.available(),
54+
// so get the length before writing to the sink
55+
contentLength();
56+
57+
mRequestBody.writeTo(mBufferedSink);
58+
mBufferedSink.flush();
4959
}
5060

51-
private Sink sink(Sink sink) {
52-
return new ForwardingSink(sink) {
53-
long bytesWritten = 0L;
54-
long contentLength = 0L;
61+
private Sink outputStreamSink(BufferedSink sink) {
62+
return Okio.sink(new CountingOutputStream(sink.outputStream()) {
63+
@Override
64+
public void write(byte[] data, int offset, int byteCount) throws IOException {
65+
super.write(data, offset, byteCount);
66+
sendProgressUpdate();
67+
}
5568

56-
@Override
57-
public void write(Buffer source, long byteCount) throws IOException {
58-
super.write(source, byteCount);
59-
if (contentLength == 0) {
60-
contentLength = contentLength();
61-
}
62-
bytesWritten += byteCount;
63-
mProgressListener.onProgress(
64-
bytesWritten, contentLength, bytesWritten == contentLength);
65-
}
66-
};
69+
@Override
70+
public void write(int data) throws IOException {
71+
super.write(data);
72+
sendProgressUpdate();
73+
}
74+
75+
private void sendProgressUpdate() throws IOException {
76+
long bytesWritten = getCount();
77+
long contentLength = contentLength();
78+
mProgressListener.onProgress(
79+
bytesWritten, contentLength, bytesWritten == contentLength);
80+
}
81+
});
6782
}
6883
}

ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ public WritableMap answer(InvocationOnMock invocation) throws Throwable {
200200

201201
@Test
202202
public void testSuccessfulPostRequest() throws Exception {
203+
RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class);
204+
ReactApplicationContext context = mock(ReactApplicationContext.class);
205+
when(context.getJSModule(any(Class.class))).thenReturn(emitter);
206+
203207
OkHttpClient httpClient = mock(OkHttpClient.class);
204208
when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer<Object>() {
205209
@Override
@@ -211,12 +215,13 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
211215
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
212216
when(clientBuilder.build()).thenReturn(httpClient);
213217
when(httpClient.newBuilder()).thenReturn(clientBuilder);
214-
NetworkingModule networkingModule =
215-
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
218+
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
216219

217220
JavaOnlyMap body = new JavaOnlyMap();
218221
body.putString("string", "This is request body");
219222

223+
mockEvents();
224+
220225
networkingModule.sendRequest(
221226
"POST",
222227
"http://somedomain/bar",

0 commit comments

Comments
 (0)