Implement XHR timeout for Android and IOS natively.
Summary: Opening this in a separate PR but the discussion can be viewed on #4832. Basically, this is a native implementation and is a bit more elegant. The consensus on my previous PR was that it should be done natively rather than in JS. There's now no maximum valid timeout value and a timeout of 0 will never time out. ontimeout isn't implemented (yet) in this PR. cc nicklockwood ide philikon Closes https://github.com/facebook/react-native/pull/5038 Reviewed By: svcscm Differential Revision: D2838743 Pulled By: nicklockwood fb-gh-sync-id: 774f864ac35082bf522f7665f4311bd3affbe82c
This commit is contained in:
parent
a3706411ab
commit
1303e6d039
|
@ -25,7 +25,7 @@ var generateRequestId = function() {
|
||||||
*/
|
*/
|
||||||
class RCTNetworking {
|
class RCTNetworking {
|
||||||
|
|
||||||
static sendRequest(method, url, headers, data, useIncrementalUpdates) {
|
static sendRequest(method, url, headers, data, useIncrementalUpdates, timeout) {
|
||||||
var requestId = generateRequestId();
|
var requestId = generateRequestId();
|
||||||
RCTNetworkingNative.sendRequest(
|
RCTNetworkingNative.sendRequest(
|
||||||
method,
|
method,
|
||||||
|
@ -33,7 +33,8 @@ class RCTNetworking {
|
||||||
requestId,
|
requestId,
|
||||||
headers,
|
headers,
|
||||||
data,
|
data,
|
||||||
useIncrementalUpdates);
|
useIncrementalUpdates,
|
||||||
|
timeout);
|
||||||
return requestId;
|
return requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,7 @@ RCT_EXPORT_MODULE()
|
||||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
|
||||||
request.HTTPMethod = [RCTConvert NSString:RCTNilIfNull(query[@"method"])].uppercaseString ?: @"GET";
|
request.HTTPMethod = [RCTConvert NSString:RCTNilIfNull(query[@"method"])].uppercaseString ?: @"GET";
|
||||||
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
|
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
|
||||||
|
request.timeoutInterval = [RCTConvert NSTimeInterval:query[@"timeout"]];
|
||||||
NSDictionary<NSString *, id> *data = [RCTConvert NSDictionary:RCTNilIfNull(query[@"data"])];
|
NSDictionary<NSString *, id> *data = [RCTConvert NSDictionary:RCTNilIfNull(query[@"data"])];
|
||||||
return [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary<NSString *, id> *result) {
|
return [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary<NSString *, id> *result) {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -26,7 +26,7 @@ function convertHeadersMapToArray(headers: Object): Array<Header> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class XMLHttpRequest extends XMLHttpRequestBase {
|
class XMLHttpRequest extends XMLHttpRequestBase {
|
||||||
sendImpl(method: ?string, url: ?string, headers: Object, data: any): void {
|
sendImpl(method: ?string, url: ?string, headers: Object, data: any, timeout: number): void {
|
||||||
var body;
|
var body;
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
body = {string: data};
|
body = {string: data};
|
||||||
|
@ -40,14 +40,14 @@ class XMLHttpRequest extends XMLHttpRequestBase {
|
||||||
} else {
|
} else {
|
||||||
body = data;
|
body = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
var useIncrementalUpdates = this.onreadystatechange ? true : false;
|
var useIncrementalUpdates = this.onreadystatechange ? true : false;
|
||||||
var requestId = RCTNetworking.sendRequest(
|
var requestId = RCTNetworking.sendRequest(
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
convertHeadersMapToArray(headers),
|
convertHeadersMapToArray(headers),
|
||||||
body,
|
body,
|
||||||
useIncrementalUpdates
|
useIncrementalUpdates,
|
||||||
|
timeout
|
||||||
);
|
);
|
||||||
this.didCreateRequest(requestId);
|
this.didCreateRequest(requestId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class XMLHttpRequest extends XMLHttpRequestBase {
|
||||||
this.upload = {};
|
this.upload = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
sendImpl(method: ?string, url: ?string, headers: Object, data: any): void {
|
sendImpl(method: ?string, url: ?string, headers: Object, data: any, timeout: number): void {
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
data = {string: data};
|
data = {string: data};
|
||||||
} else if (data instanceof FormData) {
|
} else if (data instanceof FormData) {
|
||||||
|
@ -36,6 +36,7 @@ class XMLHttpRequest extends XMLHttpRequestBase {
|
||||||
data,
|
data,
|
||||||
headers,
|
headers,
|
||||||
incrementalUpdates: this.onreadystatechange ? true : false,
|
incrementalUpdates: this.onreadystatechange ? true : false,
|
||||||
|
timeout
|
||||||
},
|
},
|
||||||
this.didCreateRequest.bind(this)
|
this.didCreateRequest.bind(this)
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,6 +32,7 @@ class XMLHttpRequestBase {
|
||||||
responseHeaders: ?Object;
|
responseHeaders: ?Object;
|
||||||
responseText: ?string;
|
responseText: ?string;
|
||||||
status: number;
|
status: number;
|
||||||
|
timeout: number;
|
||||||
responseURL: ?string;
|
responseURL: ?string;
|
||||||
|
|
||||||
upload: ?{
|
upload: ?{
|
||||||
|
@ -58,6 +59,7 @@ class XMLHttpRequestBase {
|
||||||
this.onreadystatechange = null;
|
this.onreadystatechange = null;
|
||||||
this.onload = null;
|
this.onload = null;
|
||||||
this.upload = undefined; /* Upload not supported yet */
|
this.upload = undefined; /* Upload not supported yet */
|
||||||
|
this.timeout = 0;
|
||||||
|
|
||||||
this._reset();
|
this._reset();
|
||||||
this._method = null;
|
this._method = null;
|
||||||
|
@ -196,7 +198,7 @@ class XMLHttpRequestBase {
|
||||||
this.setReadyState(this.OPENED);
|
this.setReadyState(this.OPENED);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendImpl(method: ?string, url: ?string, headers: Object, data: any): void {
|
sendImpl(method: ?string, url: ?string, headers: Object, data: any, timeout: number): void {
|
||||||
throw new Error('Subclass must define sendImpl method');
|
throw new Error('Subclass must define sendImpl method');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +210,7 @@ class XMLHttpRequestBase {
|
||||||
throw new Error('Request has already been sent');
|
throw new Error('Request has already been sent');
|
||||||
}
|
}
|
||||||
this._sent = true;
|
this._sent = true;
|
||||||
this.sendImpl(this._method, this._url, this._headers, data);
|
this.sendImpl(this._method, this._url, this._headers, data, this.timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
abort(): void {
|
abort(): void {
|
||||||
|
|
|
@ -15,6 +15,8 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import com.facebook.react.bridge.Arguments;
|
import com.facebook.react.bridge.Arguments;
|
||||||
import com.facebook.react.bridge.GuardedAsyncTask;
|
import com.facebook.react.bridge.GuardedAsyncTask;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
@ -113,19 +115,25 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
|
/**
|
||||||
|
* @param timeout value of 0 results in no timeout
|
||||||
|
*/
|
||||||
public void sendRequest(
|
public void sendRequest(
|
||||||
String method,
|
String method,
|
||||||
String url,
|
String url,
|
||||||
final int requestId,
|
final int requestId,
|
||||||
ReadableArray headers,
|
ReadableArray headers,
|
||||||
ReadableMap data,
|
ReadableMap data,
|
||||||
final boolean useIncrementalUpdates) {
|
final boolean useIncrementalUpdates,
|
||||||
|
int timeout) {
|
||||||
Request.Builder requestBuilder = new Request.Builder().url(url);
|
Request.Builder requestBuilder = new Request.Builder().url(url);
|
||||||
|
|
||||||
if (requestId != 0) {
|
if (requestId != 0) {
|
||||||
requestBuilder.tag(requestId);
|
requestBuilder.tag(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
Headers requestHeaders = extractHeaders(headers, data);
|
Headers requestHeaders = extractHeaders(headers, data);
|
||||||
if (requestHeaders == null) {
|
if (requestHeaders == null) {
|
||||||
onRequestError(requestId, "Unrecognized headers format");
|
onRequestError(requestId, "Unrecognized headers format");
|
||||||
|
|
|
@ -93,7 +93,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
SimpleArray.of(),
|
SimpleArray.of(),
|
||||||
null,
|
null,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
|
|
||||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||||
verify(httpClient).newCall(argumentCaptor.capture());
|
verify(httpClient).newCall(argumentCaptor.capture());
|
||||||
|
@ -122,7 +123,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
SimpleArray.from(invalidHeaders),
|
SimpleArray.from(invalidHeaders),
|
||||||
null,
|
null,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
|
|
||||||
verifyErrorEmit(emitter, 0);
|
verifyErrorEmit(emitter, 0);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +149,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
SimpleArray.of(),
|
SimpleArray.of(),
|
||||||
body,
|
body,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
|
|
||||||
verifyErrorEmit(emitter, 0);
|
verifyErrorEmit(emitter, 0);
|
||||||
}
|
}
|
||||||
|
@ -202,7 +205,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
SimpleArray.of(SimpleArray.of("Content-Type", "text/plain")),
|
SimpleArray.of(SimpleArray.of("Content-Type", "text/plain")),
|
||||||
body,
|
body,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
|
|
||||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||||
verify(httpClient).newCall(argumentCaptor.capture());
|
verify(httpClient).newCall(argumentCaptor.capture());
|
||||||
|
@ -238,7 +242,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
SimpleArray.from(headers),
|
SimpleArray.from(headers),
|
||||||
null,
|
null,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||||
verify(httpClient).newCall(argumentCaptor.capture());
|
verify(httpClient).newCall(argumentCaptor.capture());
|
||||||
Headers requestHeaders = argumentCaptor.getValue().headers();
|
Headers requestHeaders = argumentCaptor.getValue().headers();
|
||||||
|
@ -284,7 +289,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
new SimpleArray(),
|
new SimpleArray(),
|
||||||
body,
|
body,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
|
|
||||||
// verify url, method, headers
|
// verify url, method, headers
|
||||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||||
|
@ -341,7 +347,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
SimpleArray.from(headers),
|
SimpleArray.from(headers),
|
||||||
body,
|
body,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
|
|
||||||
// verify url, method, headers
|
// verify url, method, headers
|
||||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||||
|
@ -435,7 +442,8 @@ public class NetworkingModuleTest {
|
||||||
0,
|
0,
|
||||||
SimpleArray.from(headers),
|
SimpleArray.from(headers),
|
||||||
body,
|
body,
|
||||||
true);
|
true,
|
||||||
|
0);
|
||||||
|
|
||||||
// verify RequestBodyPart for image
|
// verify RequestBodyPart for image
|
||||||
PowerMockito.verifyStatic(times(1));
|
PowerMockito.verifyStatic(times(1));
|
||||||
|
|
Loading…
Reference in New Issue