Permalink
Browse files

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 #5038

Reviewed By: svcscm

Differential Revision: D2838743

Pulled By: nicklockwood

fb-gh-sync-id: 774f864ac35082bf522f7665f4311bd3affbe82c
  • Loading branch information...
klvs authored and facebook-github-bot-8 committed Jan 18, 2016
1 parent a370641 commit 1303e6d0392e8bc4da1198c98fc8cd7ad488b81c
@@ -25,15 +25,16 @@ var generateRequestId = function() {
*/
class RCTNetworking {
static sendRequest(method, url, headers, data, useIncrementalUpdates) {
static sendRequest(method, url, headers, data, useIncrementalUpdates, timeout) {
var requestId = generateRequestId();
RCTNetworkingNative.sendRequest(
method,
url,
requestId,
headers,
data,
useIncrementalUpdates);
useIncrementalUpdates,
timeout);
return requestId;
}
@@ -210,7 +210,7 @@ - (RCTURLRequestCancellationBlock)buildRequest:(NSDictionary<NSString *, id> *)q
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
request.HTTPMethod = [RCTConvert NSString:RCTNilIfNull(query[@"method"])].uppercaseString ?: @"GET";
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
request.timeoutInterval = [RCTConvert NSTimeInterval:query[@"timeout"]];
NSDictionary<NSString *, id> *data = [RCTConvert NSDictionary:RCTNilIfNull(query[@"data"])];
return [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary<NSString *, id> *result) {
if (error) {
@@ -26,7 +26,7 @@ function convertHeadersMapToArray(headers: Object): Array<Header> {
}
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;
if (typeof data === 'string') {
body = {string: data};
@@ -40,14 +40,14 @@ class XMLHttpRequest extends XMLHttpRequestBase {
} else {
body = data;
}
var useIncrementalUpdates = this.onreadystatechange ? true : false;
var requestId = RCTNetworking.sendRequest(
method,
url,
convertHeadersMapToArray(headers),
body,
useIncrementalUpdates
useIncrementalUpdates,
timeout
);
this.didCreateRequest(requestId);
}
@@ -23,7 +23,7 @@ class XMLHttpRequest extends XMLHttpRequestBase {
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') {
data = {string: data};
} else if (data instanceof FormData) {
@@ -36,6 +36,7 @@ class XMLHttpRequest extends XMLHttpRequestBase {
data,
headers,
incrementalUpdates: this.onreadystatechange ? true : false,
timeout
},
this.didCreateRequest.bind(this)
);
@@ -32,6 +32,7 @@ class XMLHttpRequestBase {
responseHeaders: ?Object;
responseText: ?string;
status: number;
timeout: number;
responseURL: ?string;
upload: ?{
@@ -58,6 +59,7 @@ class XMLHttpRequestBase {
this.onreadystatechange = null;
this.onload = null;
this.upload = undefined; /* Upload not supported yet */
this.timeout = 0;
this._reset();
this._method = null;
@@ -196,7 +198,7 @@ class XMLHttpRequestBase {
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');
}
@@ -208,7 +210,7 @@ class XMLHttpRequestBase {
throw new Error('Request has already been sent');
}
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 {
@@ -15,6 +15,8 @@
import java.io.InputStream;
import java.io.Reader;
import java.util.concurrent.TimeUnit;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.ReactApplicationContext;
@@ -113,19 +115,25 @@ public void onCatalystInstanceDestroy() {
}
@ReactMethod
/**
* @param timeout value of 0 results in no timeout
*/
public void sendRequest(
String method,
String url,
final int requestId,
ReadableArray headers,
ReadableMap data,
final boolean useIncrementalUpdates) {
final boolean useIncrementalUpdates,
int timeout) {
Request.Builder requestBuilder = new Request.Builder().url(url);
if (requestId != 0) {
requestBuilder.tag(requestId);
}
mClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS);

This comment has been minimized.

Show comment
Hide comment
@ajwhite

ajwhite Jan 24, 2016

Contributor

If I'm not mistaken, the mClient is a singleton instance -- wouldn't this mean that the timeout provided by a single request would apply the rule for the client that manages all requests from that point forward?

Basically this seems that it would get applied at a global level, rather than the request level

@ajwhite

ajwhite Jan 24, 2016

Contributor

If I'm not mistaken, the mClient is a singleton instance -- wouldn't this mean that the timeout provided by a single request would apply the rule for the client that manages all requests from that point forward?

Basically this seems that it would get applied at a global level, rather than the request level

Headers requestHeaders = extractHeaders(headers, data);
if (requestHeaders == null) {
onRequestError(requestId, "Unrecognized headers format");
@@ -93,7 +93,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
0,
SimpleArray.of(),
null,
true);
true,
0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
@@ -122,7 +123,8 @@ public void testFailGetWithInvalidHeadersStruct() throws Exception {
0,
SimpleArray.from(invalidHeaders),
null,
true);
true,
0);
verifyErrorEmit(emitter, 0);
}
@@ -147,7 +149,8 @@ public void testFailPostWithoutContentType() throws Exception {
0,
SimpleArray.of(),
body,
true);
true,
0);
verifyErrorEmit(emitter, 0);
}
@@ -202,7 +205,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
0,
SimpleArray.of(SimpleArray.of("Content-Type", "text/plain")),
body,
true);
true,
0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
@@ -238,7 +242,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
0,
SimpleArray.from(headers),
null,
true);
true,
0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
Headers requestHeaders = argumentCaptor.getValue().headers();
@@ -284,7 +289,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
0,
new SimpleArray(),
body,
true);
true,
0);
// verify url, method, headers
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
@@ -341,7 +347,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
0,
SimpleArray.from(headers),
body,
true);
true,
0);
// verify url, method, headers
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
@@ -435,7 +442,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
0,
SimpleArray.from(headers),
body,
true);
true,
0);
// verify RequestBodyPart for image
PowerMockito.verifyStatic(times(1));

7 comments on commit 1303e6d

@christopherdro

This comment has been minimized.

Show comment
Hide comment
@christopherdro

christopherdro Jan 20, 2016

Contributor

This is breaking for me on master. I can't seem to track down what's going on. Everything appears to be correct. Will try again in the morning.

errormsg

/cc @nicklockwood @foghina

Contributor

christopherdro replied Jan 20, 2016

This is breaking for me on master. I can't seem to track down what's going on. Everything appears to be correct. Will try again in the morning.

errormsg

/cc @nicklockwood @foghina

@klvs

This comment has been minimized.

Show comment
Hide comment
@klvs

klvs Jan 20, 2016

Contributor

Just tested it from a clean react-native init and it seems to be working fine. Unfortunately I'm having issues setting up a test with master right now and will have to wait until tomorrow to trouble shoot.

Contributor

klvs replied Jan 20, 2016

Just tested it from a clean react-native init and it seems to be working fine. Unfortunately I'm having issues setting up a test with master right now and will have to wait until tomorrow to trouble shoot.

@nicklockwood

This comment has been minimized.

Show comment
Hide comment
@nicklockwood

nicklockwood Jan 20, 2016

Contributor

@christopherdro you probably have mismatched JS and Java code versions. Seems to happen quite a lot whenever we make changes to native interfaces.

Contributor

nicklockwood replied Jan 20, 2016

@christopherdro you probably have mismatched JS and Java code versions. Seems to happen quite a lot whenever we make changes to native interfaces.

@foghina

This comment has been minimized.

Show comment
Hide comment
@foghina

foghina Jan 20, 2016

Contributor

When running on master you need to build the Android runtime from source. Otherwise, as @nicklockwood is saying, you'll end up with incompatible JS and native modules versions.

Contributor

foghina replied Jan 20, 2016

When running on master you need to build the Android runtime from source. Otherwise, as @nicklockwood is saying, you'll end up with incompatible JS and native modules versions.

@christopherdro

This comment has been minimized.

Show comment
Hide comment
@christopherdro

christopherdro Jan 20, 2016

Contributor

@nicklockwood @foghina @klvs
Yea, I figured I was missing something like that. Sorry for the bother & thanks for the tip!

Contributor

christopherdro replied Jan 20, 2016

@nicklockwood @foghina @klvs
Yea, I figured I was missing something like that. Sorry for the bother & thanks for the tip!

@jslz

This comment has been minimized.

Show comment
Hide comment
@jslz

jslz Aug 4, 2017

hi, I guess I want to use this! :-) And there's lots of people asking about support for timeouts in RN it seems. Are there docs about it? I currently use fetch() so I'd want it available there. I don't see it mentioned in docs tho https://facebook.github.io/react-native/docs/network.html

jslz replied Aug 4, 2017

hi, I guess I want to use this! :-) And there's lots of people asking about support for timeouts in RN it seems. Are there docs about it? I currently use fetch() so I'd want it available there. I don't see it mentioned in docs tho https://facebook.github.io/react-native/docs/network.html

@klvs

This comment has been minimized.

Show comment
Hide comment
@klvs

klvs Aug 5, 2017

Contributor

@jslz I believe that react-native used whatwg-fetch which doesn't include a timeout option (honestly always wondered why). Since the PR I authored implements the timeout api on XMLHttpRequest, any polyfill that uses it should have a working timeout. Maybe try whatwg-fetch-timout?

Contributor

klvs replied Aug 5, 2017

@jslz I believe that react-native used whatwg-fetch which doesn't include a timeout option (honestly always wondered why). Since the PR I authored implements the timeout api on XMLHttpRequest, any polyfill that uses it should have a working timeout. Maybe try whatwg-fetch-timout?

Please sign in to comment.