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:
Alex Roman 2016-01-18 07:47:04 -08:00 committed by facebook-github-bot-8
parent a3706411ab
commit 1303e6d039
7 changed files with 38 additions and 18 deletions

View File

@ -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;
} }

View File

@ -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) {

View File

@ -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);
} }

View File

@ -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)
); );

View File

@ -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 {

View File

@ -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");

View File

@ -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));