diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js index 991f88d4528671..0d1357152b837b 100644 --- a/Examples/UIExplorer/XHRExample.android.js +++ b/Examples/UIExplorer/XHRExample.android.js @@ -18,14 +18,15 @@ var React = require('react'); var ReactNative = require('react-native'); var { + CameraRoll, + Image, ProgressBarAndroid, StyleSheet, + Switch, Text, TextInput, TouchableHighlight, View, - Image, - CameraRoll } = ReactNative; var XHRExampleHeaders = require('./XHRExampleHeaders'); @@ -33,6 +34,13 @@ var XHRExampleCookies = require('./XHRExampleCookies'); var XHRExampleFetch = require('./XHRExampleFetch'); var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut'); +/** + * Convert number of bytes to MB and round to the nearest 0.1 MB. + */ +function roundKilo(value: number): number { + return Math.round(value / 1000); +} + // TODO t7093728 This is a simplified XHRExample.ios.js. // Once we have Camera roll, Toast, Intent (for opening URLs) // we should make this consistent with iOS. @@ -47,8 +55,18 @@ class Downloader extends React.Component { this.cancelled = false; this.state = { status: '', - contentSize: 1, - downloaded: 0, + downloading: false, + + // set by onreadystatechange + contentLength: 1, + responseLength: 0, + // set by onprogress + progressTotal: 1, + progressLoaded: 0, + + readystateHandler: false, + progressHandler: true, + arraybuffer: false, }; } @@ -56,44 +74,66 @@ class Downloader extends React.Component { this.xhr && this.xhr.abort(); var xhr = this.xhr || new XMLHttpRequest(); - xhr.onreadystatechange = () => { + const onreadystatechange = () => { if (xhr.readyState === xhr.HEADERS_RECEIVED) { - var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10); + const contentLength = parseInt(xhr.getResponseHeader('Content-Length'), 10); this.setState({ - contentSize: contentSize, - downloaded: 0, + contentLength, + responseLength: 0, }); - } else if (xhr.readyState === xhr.LOADING) { + } else if (xhr.readyState === xhr.LOADING && xhr.response) { this.setState({ - downloaded: xhr.responseText.length, + responseLength: xhr.response.length, }); - } else if (xhr.readyState === xhr.DONE) { - if (this.cancelled) { - this.cancelled = false; - return; - } - if (xhr.status === 200) { - this.setState({ - status: 'Download complete!', - }); - } else if (xhr.status !== 0) { - this.setState({ - status: 'Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText, - }); - } else { - this.setState({ - status: 'Error: ' + xhr.responseText, - }); + } + }; + const onprogress = (event) => { + this.setState({ + progressTotal: event.total, + progressLoaded: event.loaded, + }); + }; + + if (this.state.readystateHandler) { + xhr.onreadystatechange = onreadystatechange; + } + if (this.state.progressHandler) { + xhr.onprogress = onprogress; + } + if (this.state.arraybuffer) { + xhr.responseType = 'arraybuffer'; + } + xhr.onload = () => { + this.setState({downloading: false}); + if (this.cancelled) { + this.cancelled = false; + return; + } + if (xhr.status === 200) { + let responseType = `Response is a string, ${xhr.response.length} characters long.`; + if (typeof ArrayBuffer !== 'undefined' && + xhr.response instanceof ArrayBuffer) { + responseType = `Response is an ArrayBuffer, ${xhr.response.byteLength} bytes long.`; } + this.setState({status: `Download complete! ${responseType}`}); + } else if (xhr.status !== 0) { + this.setState({ + status: 'Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText + }); + } else { + this.setState({status: 'Error: ' + xhr.responseText}); } }; - xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt'); + xhr.open('GET', 'http://aleph.gutenberg.org/cache/epub/100/pg100.txt.utf8'); // Avoid gzip so we can actually show progress xhr.setRequestHeader('Accept-Encoding', ''); xhr.send(); this.xhr = xhr; - this.setState({status: 'Downloading...'}); + this.setState({ + downloading: true, + status: 'Downloading...', + }); } componentWillUnmount() { @@ -102,7 +142,7 @@ class Downloader extends React.Component { } render() { - var button = this.state.status === 'Downloading...' ? ( + var button = this.state.downloading ? ( ... @@ -118,11 +158,67 @@ class Downloader extends React.Component { ); + let readystate = null; + let progress = null; + if (this.state.readystateHandler && !this.state.arraybuffer) { + const { responseLength, contentLength } = this.state; + readystate = ( + + + responseText:{' '} + {roundKilo(responseLength)}/{roundKilo(contentLength)}k chars + + + + ); + } + if (this.state.progressHandler) { + const { progressLoaded, progressTotal } = this.state; + progress = ( + + + onprogress:{' '} + {roundKilo(progressLoaded)}/{roundKilo(progressTotal)} KB + + + + ); + } + return ( + + onreadystatechange handler + this.setState({readystateHandler}))} + /> + + + onprogress handler + this.setState({progressHandler}))} + /> + + + download as arraybuffer + this.setState({arraybuffer}))} + /> + {button} - + {readystate} + {progress} {this.state.status} ); @@ -357,6 +453,16 @@ var styles = StyleSheet.create({ backgroundColor: '#eeeeee', padding: 8, }, + progressBarLabel: { + marginTop: 12, + marginBottom: 8, + }, + configRow: { + flexDirection: 'row', + paddingVertical: 8, + alignItems: 'center', + justifyContent: 'space-between', + }, paramRow: { flexDirection: 'row', paddingVertical: 8, diff --git a/Examples/UIExplorer/XHRExample.ios.js b/Examples/UIExplorer/XHRExample.ios.js index 646c3f05bdab9c..5c49d96d0168c7 100644 --- a/Examples/UIExplorer/XHRExample.ios.js +++ b/Examples/UIExplorer/XHRExample.ios.js @@ -31,6 +31,7 @@ var { Linking, ProgressViewIOS, StyleSheet, + Switch, Text, TextInput, TouchableHighlight, @@ -41,6 +42,13 @@ var XHRExampleHeaders = require('./XHRExampleHeaders'); var XHRExampleFetch = require('./XHRExampleFetch'); var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut'); +/** + * Convert number of bytes to MB and round to the nearest 0.1 MB. + */ +function roundKilo(value: number): number { + return Math.round(value / 1000); +} + class Downloader extends React.Component { state: any; @@ -52,8 +60,16 @@ class Downloader extends React.Component { this.cancelled = false; this.state = { downloading: false, - contentSize: 1, - downloaded: 0, + // set by onreadystatechange + contentLength: 1, + responseLength: 0, + // set by onprogress + progressTotal: 1, + progressLoaded: 0, + + readystateHandler: false, + progressHandler: true, + arraybuffer: false, }; } @@ -61,41 +77,62 @@ class Downloader extends React.Component { this.xhr && this.xhr.abort(); var xhr = this.xhr || new XMLHttpRequest(); - xhr.onreadystatechange = () => { + const onreadystatechange = () => { if (xhr.readyState === xhr.HEADERS_RECEIVED) { - var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10); + const contentLength = parseInt(xhr.getResponseHeader('Content-Length'), 10); this.setState({ - contentSize: contentSize, - downloaded: 0, + contentLength, + responseLength: 0, }); } else if (xhr.readyState === xhr.LOADING) { this.setState({ - downloaded: xhr.responseText.length, + responseLength: xhr.responseText.length, }); - } else if (xhr.readyState === xhr.DONE) { - this.setState({ - downloading: false, - }); - if (this.cancelled) { - this.cancelled = false; - return; - } - if (xhr.status === 200) { - alert('Download complete!'); - } else if (xhr.status !== 0) { - alert('Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText); - } else { - alert('Error: ' + xhr.responseText); + } + }; + const onprogress = (event) => { + this.setState({ + progressTotal: event.total, + progressLoaded: event.loaded, + }); + }; + + if (this.state.readystateHandler) { + xhr.onreadystatechange = onreadystatechange; + } + if (this.state.progressHandler) { + xhr.onprogress = onprogress; + } + if (this.state.arraybuffer) { + xhr.responseType = 'arraybuffer'; + } + xhr.onload = () => { + this.setState({downloading: false}); + if (this.cancelled) { + this.cancelled = false; + return; + } + if (xhr.status === 200) { + let responseType = `Response is a string, ${xhr.response.length} characters long.`; + if (typeof ArrayBuffer !== 'undefined' && + xhr.response instanceof ArrayBuffer) { + responseType = `Response is an ArrayBuffer, ${xhr.response.byteLength} bytes long.`; } + alert(`Download complete! ${responseType}`); + } else if (xhr.status !== 0) { + alert('Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText); + } else { + alert('Error: ' + xhr.responseText); } }; - xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt'); + xhr.open('GET', 'http://aleph.gutenberg.org/cache/epub/100/pg100.txt.utf8'); xhr.send(); this.xhr = xhr; this.setState({downloading: true}); } + componentWillUnmount() { this.cancelled = true; this.xhr && this.xhr.abort(); @@ -113,15 +150,68 @@ class Downloader extends React.Component { style={styles.wrapper} onPress={this.download.bind(this)}> - Download 5MB Text File + Download 5MB Text File ); + let readystate = null; + let progress = null; + if (this.state.readystateHandler && !this.state.arraybuffer) { + const { responseLength, contentLength } = this.state; + readystate = ( + + + responseText:{' '} + {roundKilo(responseLength)}/{roundKilo(contentLength)}k chars + + + + ); + } + if (this.state.progressHandler) { + const { progressLoaded, progressTotal } = this.state; + progress = ( + + + onprogress:{' '} + {roundKilo(progressLoaded)}/{roundKilo(progressTotal)} KB + + + + ); + } + return ( + + onreadystatechange handler + this.setState({readystateHandler}))} + /> + + + onprogress handler + this.setState({progressHandler}))} + /> + + + download as arraybuffer + this.setState({arraybuffer}))} + /> + {button} - + {readystate} + {progress} ); } @@ -232,7 +322,6 @@ class FormUploader extends React.Component { (param) => formdata.append(param.name, param.value) ); xhr.upload.onprogress = (event) => { - console.log('upload onprogress', event); if (event.lengthComputable) { this.setState({uploadProgress: event.loaded / event.total}); } @@ -354,6 +443,16 @@ var styles = StyleSheet.create({ backgroundColor: '#eeeeee', padding: 8, }, + progressBarLabel: { + marginTop: 12, + marginBottom: 8, + }, + configRow: { + flexDirection: 'row', + paddingVertical: 8, + alignItems: 'center', + justifyContent: 'space-between', + }, paramRow: { flexDirection: 'row', paddingVertical: 8, diff --git a/Examples/UIExplorer/XHRExampleCookies.js b/Examples/UIExplorer/XHRExampleCookies.js index d07c7af03d7bb9..2e18d982c8fc7a 100644 --- a/Examples/UIExplorer/XHRExampleCookies.js +++ b/Examples/UIExplorer/XHRExampleCookies.js @@ -69,7 +69,7 @@ class XHRExampleCookies extends React.Component { clearCookies() { RCTNetworking.clearCookies((cleared) => { - this.setStatus('Cookies cleared, had cookies=' + cleared); + this.setStatus('Cookies cleared, had cookies=' + cleared.toString()); this.refreshWebview(); }); } diff --git a/Libraries/Network/RCTNetworkTask.h b/Libraries/Network/RCTNetworkTask.h index 3f2de225ff8ce2..189471def2f591 100644 --- a/Libraries/Network/RCTNetworkTask.h +++ b/Libraries/Network/RCTNetworkTask.h @@ -14,7 +14,7 @@ typedef void (^RCTURLRequestCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error); typedef void (^RCTURLRequestCancellationBlock)(void); -typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data); +typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data, int64_t progress, int64_t total); typedef void (^RCTURLRequestProgressBlock)(int64_t progress, int64_t total); typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response); diff --git a/Libraries/Network/RCTNetworkTask.m b/Libraries/Network/RCTNetworkTask.m index 8ce51e86240ffc..c8abb7ac0f668d 100644 --- a/Libraries/Network/RCTNetworkTask.m +++ b/Libraries/Network/RCTNetworkTask.m @@ -126,7 +126,7 @@ - (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data } [_data appendData:data]; if (_incrementalDataBlock) { - _incrementalDataBlock(data); + _incrementalDataBlock(data, _data.length, _response.expectedContentLength); } if (_downloadProgressBlock && _response.expectedContentLength > 0) { _downloadProgressBlock(_data.length, _response.expectedContentLength); diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js index 23aa9e8bf54199..4251930f65248d 100644 --- a/Libraries/Network/RCTNetworking.android.js +++ b/Libraries/Network/RCTNetworking.android.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule RCTNetworking + * @flow */ 'use strict'; @@ -27,7 +28,7 @@ function convertHeadersMapToArray(headers: Object): Array
{ } let _requestId = 1; -function generateRequestId() { +function generateRequestId(): number { return _requestId++; } @@ -41,11 +42,21 @@ class RCTNetworking extends NativeEventEmitter { super(RCTNetworkingNative); } - sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { + sendRequest( + method: string, + url: string, + headers: Object, + data: string | FormData | {uri: string}, + responseType: 'text' | 'base64', + incrementalUpdates: boolean, + timeout: number, + callback: (requestId: number) => any + ) { + let body = data; if (typeof data === 'string') { - data = {string: data}; + body = {string: body}; } else if (data instanceof FormData) { - data = { + body = { formData: data.getParts().map((part) => { part.headers = convertHeadersMapToArray(part.headers); return part; @@ -58,7 +69,8 @@ class RCTNetworking extends NativeEventEmitter { url, requestId, convertHeadersMapToArray(headers), - data, + body, + responseType, incrementalUpdates, timeout ); diff --git a/Libraries/Network/RCTNetworking.ios.js b/Libraries/Network/RCTNetworking.ios.js index 3e0e4c35284119..a6cb9163cc2090 100644 --- a/Libraries/Network/RCTNetworking.ios.js +++ b/Libraries/Network/RCTNetworking.ios.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule RCTNetworking + * @flow */ 'use strict'; @@ -20,27 +21,38 @@ class RCTNetworking extends NativeEventEmitter { super(RCTNetworkingNative); } - sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { + sendRequest( + method: string, + url: string, + headers: Object, + data: string | FormData | {uri: string}, + responseType: 'text' | 'base64', + incrementalUpdates: boolean, + timeout: number, + callback: (requestId: number) => any + ) { + let body = data; if (typeof data === 'string') { - data = {string: data}; + body = {string: data}; } else if (data instanceof FormData) { - data = {formData: data.getParts()}; + body = {formData: data.getParts()}; } RCTNetworkingNative.sendRequest({ method, url, - data, + body, headers, + responseType, incrementalUpdates, timeout }, callback); } - abortRequest(requestId) { + abortRequest(requestId: number) { RCTNetworkingNative.abortRequest(requestId); } - clearCookies(callback) { + clearCookies(callback: (result: boolean) => any) { console.warn('RCTNetworking.clearCookies is not supported on iOS'); } } diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index 07c21f095d3fad..7a92a161105323 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -138,6 +138,8 @@ @implementation RCTNetworking return @[@"didCompleteNetworkResponse", @"didReceiveNetworkResponse", @"didSendNetworkData", + @"didReceiveNetworkIncrementalData", + @"didReceiveNetworkDataProgress", @"didReceiveNetworkData"]; } @@ -313,26 +315,16 @@ - (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(nullable NSDictionary return callback(nil, nil); } -- (void)sendData:(NSData *)data forTask:(RCTNetworkTask *)task ++ (NSString *)decodeTextData:(NSData *)data fromResponse:(NSURLResponse *)response { - RCTAssertThread(_methodQueue, @"sendData: must be called on method queue"); - - if (data.length == 0) { - return; - } - - // Get text encoding - NSURLResponse *response = task.response; NSStringEncoding encoding = NSUTF8StringEncoding; if (response.textEncodingName) { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } - // Attempt to decode text - NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding]; - if (!responseText && data.length) { - + NSString *encodedResponse = [[NSString alloc] initWithData:data encoding:encoding]; + if (!encodedResponse && data.length) { // We don't have an encoding, or the encoding is incorrect, so now we // try to guess (unfortunately, this feature is available in iOS 8+ only) if ([NSString respondsToSelector:@selector(stringEncodingForData: @@ -341,22 +333,43 @@ - (void)sendData:(NSData *)data forTask:(RCTNetworkTask *)task usedLossyConversion:)]) { [NSString stringEncodingForData:data encodingOptions:nil - convertedString:&responseText + convertedString:&encodedResponse usedLossyConversion:NULL]; } + } + return encodedResponse; +} - // If we still can't decode it, bail out - if (!responseText) { +- (void)sendData:(NSData *)data + responseType:(NSString *)responseType + forTask:(RCTNetworkTask *)task +{ + RCTAssertThread(_methodQueue, @"sendData: must be called on method queue"); + + if (data.length == 0) { + return; + } + + NSString *responseString; + if ([responseType isEqualToString:@"text"]) { + responseString = [RCTNetworking decodeTextData:data fromResponse:task.response]; + if (!responseString) { RCTLogWarn(@"Received data was not a string, or was not a recognised encoding."); return; } + } else if ([responseType isEqualToString:@"base64"]) { + responseString = [data base64EncodedStringWithOptions:0]; + } else { + RCTLogWarn(@"Invalid responseType: %@", responseType); + return; } - NSArray *responseJSON = @[task.requestID, responseText ?: @""]; + NSArray *responseJSON = @[task.requestID, responseString]; [self sendEventWithName:@"didReceiveNetworkData" body:responseJSON]; } - (void)sendRequest:(NSURLRequest *)request + responseType:(NSString *)responseType incrementalUpdates:(BOOL)incrementalUpdates responseSender:(RCTResponseSenderBlock)responseSender { @@ -371,7 +384,7 @@ - (void)sendRequest:(NSURLRequest *)request }); }; - void (^responseBlock)(NSURLResponse *) = ^(NSURLResponse *response) { + RCTURLRequestResponseBlock responseBlock = ^(NSURLResponse *response) { dispatch_async(_methodQueue, ^{ NSDictionary *headers; NSInteger status; @@ -389,17 +402,44 @@ - (void)sendRequest:(NSURLRequest *)request }); }; - void (^incrementalDataBlock)(NSData *) = incrementalUpdates ? ^(NSData *data) { - dispatch_async(_methodQueue, ^{ - [self sendData:data forTask:task]; - }); - } : nil; + // XHR does not allow you to peek at xhr.response before the response is + // finished. Only when xhr.responseType is set to ''/'text', consumers may + // peek at xhr.responseText. So unless the requested responseType is 'text', + // we only send progress updates and not incremental data updates to JS here. + RCTURLRequestIncrementalDataBlock incrementalDataBlock = nil; + RCTURLRequestProgressBlock downloadProgressBlock = nil; + if (incrementalUpdates) { + if ([responseType isEqualToString:@"text"]) { + incrementalDataBlock = ^(NSData *data, int64_t progress, int64_t total) { + dispatch_async(_methodQueue, ^{ + NSString *responseString = [RCTNetworking decodeTextData:data fromResponse:task.response]; + if (!responseString) { + RCTLogWarn(@"Received data was not a string, or was not a recognised encoding."); + return; + } + NSArray *responseJSON = @[task.requestID, responseString, @(progress), @(total)]; + [self sendEventWithName:@"didReceiveNetworkIncrementalData" body:responseJSON]; + }); + }; + } else { + downloadProgressBlock = ^(int64_t progress, int64_t total) { + dispatch_async(_methodQueue, ^{ + NSArray *responseJSON = @[task.requestID, @(progress), @(total)]; + [self sendEventWithName:@"didReceiveNetworkDataProgress" body:responseJSON]; + }); + }; + } + } RCTURLRequestCompletionBlock completionBlock = ^(NSURLResponse *response, NSData *data, NSError *error) { dispatch_async(_methodQueue, ^{ - if (!incrementalUpdates) { - [self sendData:data forTask:task]; + // Unless we were sending incremental (text) chunks to JS, all along, now + // is the time to send the request body to JS. + if (!(incrementalUpdates && [responseType isEqualToString:@"text"])) { + [self sendData:data + responseType:responseType + forTask:task]; } NSArray *responseJSON = @[task.requestID, RCTNullIfNil(error.localizedDescription), @@ -412,6 +452,7 @@ - (void)sendRequest:(NSURLRequest *)request }; task = [self networkTaskWithRequest:request completionBlock:completionBlock]; + task.downloadProgressBlock = downloadProgressBlock; task.incrementalDataBlock = incrementalDataBlock; task.responseBlock = responseBlock; task.uploadProgressBlock = uploadProgressBlock; @@ -453,8 +494,10 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request // loading a large file to build the request body [self buildRequest:query completionBlock:^(NSURLRequest *request) { + NSString *responseType = [RCTConvert NSString:query[@"responseType"]]; BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]]; [self sendRequest:request + responseType:responseType incrementalUpdates:incrementalUpdates responseSender:responseSender]; }]; diff --git a/Libraries/Network/XMLHttpRequest.js b/Libraries/Network/XMLHttpRequest.js index ed39d9fe450070..c165079316d5b9 100644 --- a/Libraries/Network/XMLHttpRequest.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -14,8 +14,8 @@ const RCTNetworking = require('RCTNetworking'); const EventTarget = require('event-target-shim'); +const base64 = require('base64-js'); const invariant = require('fbjs/lib/invariant'); -const utf8 = require('utf8'); const warning = require('fbjs/lib/warning'); type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text'; @@ -102,7 +102,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { _method: ?string = null; _response: string | ?Object; _responseType: ResponseType; - _responseText: string = ''; + _response: string = ''; _sent: boolean; _url: ?string = null; _timedOut: boolean = false; @@ -124,7 +124,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { this._cachedResponse = undefined; this._hasError = false; this._headers = {}; - this._responseText = ''; + this._response = ''; this._responseType = ''; this._sent = false; this._lowerCaseResponseHeaders = {}; @@ -140,10 +140,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { // $FlowIssue #10784535 set responseType(responseType: ResponseType): void { - if (this.readyState > HEADERS_RECEIVED) { + if (this._sent) { throw new Error( - "Failed to set the 'responseType' property on 'XMLHttpRequest': The " + - "response type cannot be set if the object's state is LOADING or DONE" + 'Failed to set the \'responseType\' property on \'XMLHttpRequest\': The ' + + 'response type cannot be set after the request has been sent.' ); } if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) { @@ -173,7 +173,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { if (this.readyState < LOADING) { return ''; } - return this._responseText; + return this._response; } // $FlowIssue #10784535 @@ -182,7 +182,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { if (responseType === '' || responseType === 'text') { return this.readyState < LOADING || this._hasError ? '' - : this._responseText; + : this._response; } if (this.readyState !== DONE) { @@ -193,26 +193,25 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { return this._cachedResponse; } - switch (this._responseType) { + switch (responseType) { case 'document': this._cachedResponse = null; break; case 'arraybuffer': - this._cachedResponse = toArrayBuffer( - this._responseText, this.getResponseHeader('content-type') || ''); + this._cachedResponse = base64.toByteArray(this._response).buffer; break; case 'blob': this._cachedResponse = new global.Blob( - [this._responseText], + [base64.toByteArray(this._response).buffer], {type: this.getResponseHeader('content-type') || ''} ); break; case 'json': try { - this._cachedResponse = JSON.parse(this._responseText); + this._cachedResponse = JSON.parse(this._response); } catch (_) { this._cachedResponse = null; } @@ -231,7 +230,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { } // exposed for testing - __didUploadProgress(requestId: number, progress: number, total: number): void { + __didUploadProgress( + requestId: number, + progress: number, + total: number + ): void { if (requestId === this._requestId) { this.upload.dispatchEvent({ type: 'progress', @@ -260,16 +263,47 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { } } - __didReceiveData(requestId: number, responseText: string): void { - if (requestId === this._requestId) { - if (!this._responseText) { - this._responseText = responseText; - } else { - this._responseText += responseText; - } - this._cachedResponse = undefined; // force lazy recomputation - this.setReadyState(this.LOADING); + __didReceiveData(requestId: number, response: string): void { + if (requestId !== this._requestId) { + return; + } + this._response = response; + this._cachedResponse = undefined; // force lazy recomputation + this.setReadyState(this.LOADING); + } + + __didReceiveIncrementalData( + requestId: number, + responseText: string, + progress: number, + total: number + ) { + if (requestId !== this._requestId) { + return; + } + if (!this._response) { + this._response = responseText; + } else { + this._response += responseText; + } + this.setReadyState(this.LOADING); + this.__didReceiveDataProgress(requestId, progress, total); + } + + __didReceiveDataProgress( + requestId: number, + loaded: number, + total: number + ): void { + if (requestId !== this._requestId) { + return; } + this.dispatchEvent({ + type: 'progress', + lengthComputable: total >= 0, + loaded, + total, + }); } // exposed for testing @@ -280,7 +314,9 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { ): void { if (requestId === this._requestId) { if (error) { - this._responseText = error; + if (this._responseType === '' || this._responseType === 'text') { + this._response = error; + } this._hasError = true; if (timeOutError) { this._timedOut = true; @@ -334,21 +370,24 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { if (!url) { throw new Error('Cannot load an empty url'); } - this._reset(); this._method = method.toUpperCase(); this._url = url; this._aborted = false; this.setReadyState(this.OPENED); } - sendImpl( - method: ?string, - url: ?string, - headers: Object, - data: any, - useIncrementalUpdates: boolean, - timeout: number, - ): void { + send(data: any): void { + if (this.readyState !== this.OPENED) { + throw new Error('Request has not been opened'); + } + if (this._sent) { + throw new Error('Request has already been sent'); + } + this._sent = true; + const incrementalEvents = this._incrementalEvents || + !!this.onreadystatechange || + !!this.onprogress; + this._subscriptions.push(RCTNetworking.addListener( 'didSendNetworkData', (args) => this.__didUploadProgress(...args) @@ -359,39 +398,37 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { )); this._subscriptions.push(RCTNetworking.addListener( 'didReceiveNetworkData', - (args) => this.__didReceiveData(...args) + (args) => this.__didReceiveData(...args) + )); + this._subscriptions.push(RCTNetworking.addListener( + 'didReceiveNetworkIncrementalData', + (args) => this.__didReceiveIncrementalData(...args) + )); + this._subscriptions.push(RCTNetworking.addListener( + 'didReceiveNetworkDataProgress', + (args) => this.__didReceiveDataProgress(...args) )); this._subscriptions.push(RCTNetworking.addListener( 'didCompleteNetworkResponse', (args) => this.__didCompleteResponse(...args) )); - RCTNetworking.sendRequest( - method, - url, - headers, - data, - useIncrementalUpdates, - timeout, - this.__didCreateRequest.bind(this), - ); - } - send(data: any): void { - if (this.readyState !== this.OPENED) { - throw new Error('Request has not been opened'); + let nativeResponseType = 'text'; + if (this._responseType === 'arraybuffer' || this._responseType === 'blob') { + nativeResponseType = 'base64'; } - if (this._sent) { - throw new Error('Request has already been sent'); - } - this._sent = true; - const incrementalEvents = this._incrementalEvents || !!this.onreadystatechange; - this.sendImpl( + + invariant(this._method, 'Request method needs to be defined.'); + invariant(this._url, 'Request URL needs to be defined.'); + RCTNetworking.sendRequest( this._method, this._url, this._headers, data, + nativeResponseType, incrementalEvents, - this.timeout + this.timeout, + this.__didCreateRequest.bind(this), ); } @@ -444,32 +481,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { // have to send repeated LOADING events with incremental updates // to responseText, which will avoid a bunch of native -> JS // bridge traffic. - if (type === 'readystatechange') { + if (type === 'readystatechange' || type === 'progress') { this._incrementalEvents = true; } super.addEventListener(type, listener); } } - -function toArrayBuffer(text: string, contentType: string): ArrayBuffer { - const {length} = text; - if (length === 0) { - return new ArrayBuffer(0); - } - - const charsetMatch = contentType.match(/;\s*charset=([^;]*)/i); - const charset = charsetMatch ? charsetMatch[1].trim() : 'utf-8'; - - if (/^utf-?8$/i.test(charset)) { - return utf8.encode(text); - } else { //TODO: utf16 / ucs2 / utf32 - const array = new Uint8Array(length); - for (let i = 0; i < length; i++) { - array[i] = text.charCodeAt(i); // Uint8Array automatically masks with 0xff - } - return array.buffer; - } -} - module.exports = XMLHttpRequest; diff --git a/Libraries/Network/__tests__/XMLHttpRequest-test.js b/Libraries/Network/__tests__/XMLHttpRequest-test.js index afbae4780b080f..db82c60f736b4f 100644 --- a/Libraries/Network/__tests__/XMLHttpRequest-test.js +++ b/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -10,18 +10,22 @@ 'use strict'; jest - .disableAutomock() - .dontMock('event-target-shim') - .setMock('NativeModules', { + .disableAutomock() + .dontMock('event-target-shim') + .setMock('NativeModules', { Networking: { - addListener: function(){}, - removeListeners: function(){}, + addListener: function() {}, + removeListeners: function() {}, + sendRequest: (options, callback) => { + callback(1); + }, + abortRequest: function() {}, } }); const XMLHttpRequest = require('XMLHttpRequest'); -describe('XMLHttpRequest', function(){ +describe('XMLHttpRequest', function() { var xhr; var handleTimeout; var handleError; @@ -45,8 +49,6 @@ describe('XMLHttpRequest', function(){ xhr.addEventListener('error', handleError); xhr.addEventListener('load', handleLoad); xhr.addEventListener('readystatechange', handleReadyStateChange); - - xhr.__didCreateRequest(1); }); afterEach(() => { @@ -57,8 +59,7 @@ describe('XMLHttpRequest', function(){ }); it('should transition readyState correctly', function() { - - expect(xhr.readyState).toBe(xhr.UNSENT); + expect(xhr.readyState).toBe(xhr.UNSENT); xhr.open('GET', 'blabla'); @@ -78,7 +79,8 @@ describe('XMLHttpRequest', function(){ expect(xhr.responseType).toBe('arraybuffer'); // Can't change responseType after first data has been received. - xhr.__didReceiveData(1, 'Some data'); + xhr.open('GET', 'blabla'); + xhr.send(); expect(() => { xhr.responseType = 'text'; }).toThrow(); }); @@ -100,11 +102,16 @@ describe('XMLHttpRequest', function(){ expect(xhr.responseText).toBe(''); expect(xhr.response).toBe(''); + xhr.open('GET', 'blabla'); + xhr.send(); xhr.__didReceiveData(1, 'Some data'); expect(xhr.responseText).toBe('Some data'); }); - it('should call ontimeout function when the request times out', function(){ + it('should call ontimeout function when the request times out', function() { + xhr.open('GET', 'blabla'); + xhr.send(); + xhr.__didCompleteResponse(1, 'Timeout', true); xhr.__didCompleteResponse(1, 'Timeout', true); expect(xhr.readyState).toBe(xhr.DONE); @@ -118,39 +125,46 @@ describe('XMLHttpRequest', function(){ expect(handleLoad).not.toBeCalled(); }); - it('should call onerror function when the request times out', function(){ + it('should call onerror function when the request times out', function() { + xhr.open('GET', 'blabla'); + xhr.send(); xhr.__didCompleteResponse(1, 'Generic error'); expect(xhr.readyState).toBe(xhr.DONE); - expect(xhr.onreadystatechange.mock.calls.length).toBe(1); + expect(xhr.onreadystatechange.mock.calls.length).toBe(2); expect(xhr.onerror.mock.calls.length).toBe(1); expect(xhr.ontimeout).not.toBeCalled(); expect(xhr.onload).not.toBeCalled(); - expect(handleReadyStateChange.mock.calls.length).toBe(1); + expect(handleReadyStateChange.mock.calls.length).toBe(2); expect(handleError.mock.calls.length).toBe(1); expect(handleTimeout).not.toBeCalled(); expect(handleLoad).not.toBeCalled(); }); - it('should call onload function when there is no error', function(){ + it('should call onload function when there is no error', function() { + xhr.open('GET', 'blabla'); + xhr.send(); xhr.__didCompleteResponse(1, null); expect(xhr.readyState).toBe(xhr.DONE); - expect(xhr.onreadystatechange.mock.calls.length).toBe(1); + expect(xhr.onreadystatechange.mock.calls.length).toBe(2); expect(xhr.onload.mock.calls.length).toBe(1); expect(xhr.onerror).not.toBeCalled(); expect(xhr.ontimeout).not.toBeCalled(); - expect(handleReadyStateChange.mock.calls.length).toBe(1); + expect(handleReadyStateChange.mock.calls.length).toBe(2); expect(handleLoad.mock.calls.length).toBe(1); expect(handleError).not.toBeCalled(); expect(handleTimeout).not.toBeCalled(); }); it('should call onload function when there is no error', function() { + xhr.open('GET', 'blabla'); + xhr.send(); + xhr.upload.onprogress = jest.fn(); var handleProgress = jest.fn(); xhr.upload.addEventListener('progress', handleProgress); 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 9c42dfcd59130e..f321dbf4e39300 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 @@ -9,6 +9,8 @@ package com.facebook.react.modules.network; +import android.util.Base64; + import javax.annotation.Nullable; import java.io.IOException; @@ -34,6 +36,7 @@ import okhttp3.Call; import okhttp3.Callback; import okhttp3.Headers; +import okhttp3.Interceptor; import okhttp3.JavaNetCookieJar; import okhttp3.MediaType; import okhttp3.MultipartBody; @@ -157,6 +160,7 @@ public void sendRequest( final int requestId, ReadableArray headers, ReadableMap data, + final String responseType, final boolean useIncrementalUpdates, int timeout) { Request.Builder requestBuilder = new Request.Builder().url(url); @@ -165,18 +169,54 @@ public void sendRequest( requestBuilder.tag(requestId); } - OkHttpClient client = mClient; + final RCTDeviceEventEmitter eventEmitter = getEventEmitter(executorToken); + OkHttpClient.Builder clientBuilder = mClient.newBuilder(); + + // If JS is listening for progress updates, install a ProgressResponseBody that intercepts the + // response and counts bytes received. + if (useIncrementalUpdates) { + clientBuilder.addNetworkInterceptor(new Interceptor() { + @Override + public Response intercept(Interceptor.Chain chain) throws IOException { + Response originalResponse = chain.proceed(chain.request()); + ProgressResponseBody responseBody = new ProgressResponseBody( + originalResponse.body(), + new ProgressListener() { + long last = System.nanoTime(); + + @Override + public void onProgress(long bytesWritten, long contentLength, boolean done) { + long now = System.nanoTime(); + if (!done && !shouldDispatch(now, last)) { + return; + } + if (responseType.equals("text")) { + // For 'text' responses we continuously send response data with progress info to JS + // below, so no need to do anything here. + return; + } + ResponseUtil.onDataReceivedProgress( + eventEmitter, + requestId, + bytesWritten, + contentLength); + last = now; + } + }); + return originalResponse.newBuilder().body(responseBody).build(); + } + }); + } + // If the current timeout does not equal the passed in timeout, we need to clone the existing // client and set the timeout explicitly on the clone. This is cheap as everything else is // shared under the hood. // See https://github.com/square/okhttp/wiki/Recipes#per-call-configuration for more information if (timeout != mClient.connectTimeoutMillis()) { - client = mClient.newBuilder() - .readTimeout(timeout, TimeUnit.MILLISECONDS) - .build(); + clientBuilder.readTimeout(timeout, TimeUnit.MILLISECONDS); } + OkHttpClient client = clientBuilder.build(); - final RCTDeviceEventEmitter eventEmitter = getEventEmitter(executorToken); Headers requestHeaders = extractHeaders(headers, data); if (requestHeaders == null) { ResponseUtil.onRequestError(eventEmitter, requestId, "Unrecognized headers format", null); @@ -247,11 +287,11 @@ public void sendRequest( method, RequestBodyUtil.createProgressRequest( multipartBuilder.build(), - new ProgressRequestListener() { + new ProgressListener() { long last = System.nanoTime(); @Override - public void onRequestProgress(long bytesWritten, long contentLength, boolean done) { + public void onProgress(long bytesWritten, long contentLength, boolean done) { long now = System.nanoTime(); if (done || shouldDispatch(now, last)) { ResponseUtil.onDataSend(eventEmitter, requestId, bytesWritten, contentLength); @@ -292,13 +332,23 @@ public void onResponse(Call call, Response response) throws IOException { ResponseBody responseBody = response.body(); try { - if (useIncrementalUpdates) { + // If JS wants progress updates during the download, and it requested a text response, + // periodically send response data updates to JS. + if (useIncrementalUpdates && responseType.equals("text")) { readWithProgress(eventEmitter, requestId, responseBody); ResponseUtil.onRequestSuccess(eventEmitter, requestId); - } else { - ResponseUtil.onDataReceived(eventEmitter, requestId, responseBody.string()); - ResponseUtil.onRequestSuccess(eventEmitter, requestId); + return; } + + // Otherwise send the data in one big chunk, in the format that JS requested. + String responseString = ""; + if (responseType.equals("text")) { + responseString = responseBody.string(); + } else if (responseType.equals("base64")) { + responseString = Base64.encodeToString(responseBody.bytes(), Base64.NO_WRAP); + } + ResponseUtil.onDataReceived(eventEmitter, requestId, responseString); + ResponseUtil.onRequestSuccess(eventEmitter, requestId); } catch (IOException e) { ResponseUtil.onRequestError(eventEmitter, requestId, e.getMessage(), e); } @@ -311,11 +361,17 @@ private void readWithProgress( int requestId, ResponseBody responseBody) throws IOException { Reader reader = responseBody.charStream(); + ProgressResponseBody progressResponseBody = (ProgressResponseBody)responseBody; try { char[] buffer = new char[MAX_CHUNK_SIZE_BETWEEN_FLUSHES]; int read; while ((read = reader.read(buffer)) != -1) { - ResponseUtil.onDataReceived(eventEmitter, requestId, new String(buffer, 0, read)); + ResponseUtil.onIncrementalDataReceived( + eventEmitter, + requestId, + new String(buffer, 0, read), + progressResponseBody.totalBytesRead(), + progressResponseBody.contentLength()); } } finally { reader.close(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressListener.java similarity index 73% rename from ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java rename to ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressListener.java index 10230e6dcb9c56..08a61797c86ae6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressListener.java @@ -6,10 +6,10 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ - + package com.facebook.react.modules.network; -public interface ProgressRequestListener { - void onRequestProgress(long bytesWritten, long contentLength, boolean done); +public interface ProgressListener { + void onProgress(long bytesWritten, long contentLength, boolean done); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java index 0e511f926dda19..4955a2748bff6a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java @@ -12,22 +12,19 @@ import java.io.IOException; import okhttp3.MediaType; import okhttp3.RequestBody; -import okhttp3.internal.Util; import okio.BufferedSink; import okio.Buffer; import okio.Sink; import okio.ForwardingSink; -import okio.ByteString; import okio.Okio; -import okio.Source; public class ProgressRequestBody extends RequestBody { private final RequestBody mRequestBody; - private final ProgressRequestListener mProgressListener; + private final ProgressListener mProgressListener; private BufferedSink mBufferedSink; - public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) { + public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) { mRequestBody = requestBody; mProgressListener = progressListener; } @@ -63,7 +60,7 @@ public void write(Buffer source, long byteCount) throws IOException { contentLength = contentLength(); } bytesWritten += byteCount; - mProgressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength); + mProgressListener.onProgress(bytesWritten, contentLength, bytesWritten == contentLength); } }; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressResponseBody.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressResponseBody.java new file mode 100644 index 00000000000000..2fdf23961b055d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressResponseBody.java @@ -0,0 +1,59 @@ +package com.facebook.react.modules.network; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +public class ProgressResponseBody extends ResponseBody { + + private final ResponseBody mResponseBody; + private final ProgressListener mProgressListener; + private BufferedSource mBufferedSource; + private long mTotalBytesRead; + + public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { + this.mResponseBody = responseBody; + this.mProgressListener = progressListener; + mTotalBytesRead = 0L; + } + + + @Override + public MediaType contentType() { + return mResponseBody.contentType(); + } + + @Override + public long contentLength() { + return mResponseBody.contentLength(); + } + + public long totalBytesRead() { + return mTotalBytesRead; + } + + @Override public BufferedSource source() { + if (mBufferedSource == null) { + mBufferedSource = Okio.buffer(source(mResponseBody.source())); + } + return mBufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + @Override public long read(Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + // read() returns the number of bytes read, or -1 if this source is exhausted. + mTotalBytesRead += bytesRead != -1 ? bytesRead : 0; + mProgressListener.onProgress(mTotalBytesRead, mResponseBody.contentLength(), bytesRead == -1); + return bytesRead; + } + }; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java index 1d5a5e1d916a05..004b15616795d4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java @@ -117,7 +117,7 @@ public void writeTo(BufferedSink sink) throws IOException { /** * Creates a ProgressRequestBody that can be used for showing uploading progress */ - public static ProgressRequestBody createProgressRequest(RequestBody requestBody, ProgressRequestListener listener) { + public static ProgressRequestBody createProgressRequest(RequestBody requestBody, ProgressListener listener) { return new ProgressRequestBody(requestBody, listener); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java index eb2b6e2bbb6a5d..5b589e195b0ccc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java @@ -33,6 +33,34 @@ public static void onDataSend( eventEmitter.emit("didSendNetworkData", args); } + public static void onIncrementalDataReceived( + RCTDeviceEventEmitter eventEmitter, + int requestId, + String data, + long progress, + long total) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushString(data); + args.pushInt((int) progress); + args.pushInt((int) total); + + eventEmitter.emit("didReceiveNetworkIncrementalData", args); + } + + public static void onDataReceivedProgress( + RCTDeviceEventEmitter eventEmitter, + int requestId, + long progress, + long total) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushInt((int) progress); + args.pushInt((int) total); + + eventEmitter.emit("didReceiveNetworkDataProgress", args); + } + public static void onDataReceived( RCTDeviceEventEmitter eventEmitter, int requestId, 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 c06a99f9adef47..3c61238c5d9c7f 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 @@ -61,11 +61,12 @@ Call.class, RequestBodyUtil.class, ProgressRequestBody.class, - ProgressRequestListener.class, + ProgressListener.class, MultipartBody.class, MultipartBody.Builder.class, NetworkingModule.class, OkHttpClient.class, + OkHttpClient.Builder.class, OkHttpCallUtil.class}) @RunWith(RobolectricTestRunner.class) @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) @@ -84,6 +85,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); @@ -91,11 +95,12 @@ public Object answer(InvocationOnMock invocation) throws Throwable { mock(ExecutorToken.class), "GET", "http://somedomain/foo", - 0, - JavaOnlyArray.of(), - null, - true, - 0); + /* requestId */ 0, + /* headers */ JavaOnlyArray.of(), + /* body */ null, + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); verify(httpClient).newCall(argumentCaptor.capture()); @@ -112,6 +117,9 @@ public void testFailGetWithInvalidHeadersStruct() throws Exception { when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter); OkHttpClient httpClient = mock(OkHttpClient.class); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient); List invalidHeaders = Arrays.asList(JavaOnlyArray.of("foo")); @@ -122,11 +130,12 @@ public void testFailGetWithInvalidHeadersStruct() throws Exception { mock(ExecutorToken.class), "GET", "http://somedoman/foo", - 0, - JavaOnlyArray.from(invalidHeaders), - null, - true, - 0); + /* requestId */ 0, + /* headers */ JavaOnlyArray.from(invalidHeaders), + /* body */ null, + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); verifyErrorEmit(emitter, 0); } @@ -138,6 +147,9 @@ public void testFailPostWithoutContentType() throws Exception { when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter); OkHttpClient httpClient = mock(OkHttpClient.class); + 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(); @@ -152,8 +164,9 @@ public void testFailPostWithoutContentType() throws Exception { 0, JavaOnlyArray.of(), body, - true, - 0); + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); verifyErrorEmit(emitter, 0); } @@ -196,6 +209,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); @@ -209,8 +225,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { 0, JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain")), body, - true, - 0); + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); verify(httpClient).newCall(argumentCaptor.capture()); @@ -234,6 +251,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); @@ -248,8 +268,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { 0, JavaOnlyArray.from(headers), null, - true, - 0); + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); verify(httpClient).newCall(argumentCaptor.capture()); Headers requestHeaders = argumentCaptor.getValue().headers(); @@ -265,7 +286,7 @@ public void testMultipartPostRequestSimple() throws Exception { .thenReturn(mock(InputStream.class)); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) .thenReturn(mock(RequestBody.class)); - when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class))).thenCallRealMethod(); JavaOnlyMap body = new JavaOnlyMap(); JavaOnlyArray formData = new JavaOnlyArray(); @@ -288,6 +309,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.sendRequest( @@ -297,8 +321,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { 0, new JavaOnlyArray(), body, - true, - 0); + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); // verify url, method, headers ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); @@ -320,7 +345,7 @@ public void testMultipartPostRequestHeaders() throws Exception { .thenReturn(mock(InputStream.class)); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) .thenReturn(mock(RequestBody.class)); - when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class))).thenCallRealMethod(); List headers = Arrays.asList( JavaOnlyArray.of("Accept", "text/plain"), @@ -348,6 +373,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.sendRequest( @@ -357,8 +385,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { 0, JavaOnlyArray.from(headers), body, - true, - 0); + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); // verify url, method, headers ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); @@ -383,7 +412,7 @@ public void testMultipartPostRequestBody() throws Exception { when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) .thenReturn(inputStream); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))).thenCallRealMethod(); - when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class))).thenCallRealMethod(); when(inputStream.available()).thenReturn("imageUri".length()); final MultipartBody.Builder multipartBuilder = mock(MultipartBody.Builder.class); @@ -445,6 +474,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); @@ -455,8 +487,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { 0, JavaOnlyArray.from(headers), body, - true, - 0); + /* responseType */ "text", + /* useIncrementalUpdates*/ true, + /* timeout */ 0); // verify RequestBodyPart for image PowerMockito.verifyStatic(times(1)); @@ -503,6 +536,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return calls[(Integer) request.tag() - 1]; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.initialize(); @@ -515,7 +551,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { idx + 1, JavaOnlyArray.of(), null, - true, + /* responseType */ "text", + /* useIncrementalUpdates*/ true, 0); } verify(httpClient, times(3)).newCall(any(Request.class)); @@ -550,6 +587,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return calls[(Integer) request.tag() - 1]; } }); + OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); NetworkingModule networkingModule = new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); @@ -561,7 +601,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { idx + 1, JavaOnlyArray.of(), null, - true, + /* responseType */ "text", + /* useIncrementalUpdates*/ true, 0); } verify(httpClient, times(3)).newCall(any(Request.class));