diff --git a/README.md b/README.md index c656f12..a506eea 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,46 @@ cordova.plugin.http.downloadFile("https://google.com/", { }); ``` +### abort +Abort a HTTP request. Takes the `requestId` which is returned by [sendRequest](#sendRequest) and its shorthand functions ([post](#post), [get](#get), [put](#put), [patch](#patch), [delete](#delete), [head](#head), [uploadFile](#uploadFile) and [downloadFile](#downloadFile)). + +If the request already has finished, the request will finish normally and the abort call result will be `{ aborted: false }`. + +If the request is still in progress, the request's `failure` callback will be invoked with response `{ status: -8 }`, and the abort call result `{ aborted: true }`. + +:warning: Not supported for Android < 6 (API level < 23). For Android 5.1 and below, calling `abort(reqestId)` will have no effect, i.e. the requests will finish as if the request was not cancelled. + +```js +// start a request and get its requestId +var requestId = cordova.plugin.http.downloadFile("https://google.com/", { + id: '12', + message: 'test' +}, { Authorization: 'OAuth2: token' }, 'file:///somepicture.jpg', function(entry) { + // prints the filename + console.log(entry.name); + + // prints the filePath + console.log(entry.fullPath); +}, function(response) { + // if request was actually aborted, failure callback with status -8 will be invoked + if(response.status === -8){ + console.log('download aborted'); + } else { + console.error(response.error); + } +}); + +//... + +// abort request +cordova.plugin.http.abort(requestId, function(result) { + // prints if request was aborted: true | false + console.log(result.aborted); +}, function(response) { + console.error(response.error); +}); +``` + ## Browser support This plugin supports a very restricted set of functions on the browser platform. diff --git a/plugin.xml b/plugin.xml index 82ec955..12685c6 100644 --- a/plugin.xml +++ b/plugin.xml @@ -74,6 +74,7 @@ + @@ -92,4 +93,4 @@ - \ No newline at end of file + diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java index e56be1c..c107e3a 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java @@ -5,6 +5,7 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.IOException; +import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -18,8 +19,6 @@ import com.silkimen.http.HttpRequest.HttpRequestException; import com.silkimen.http.JsonUtils; import com.silkimen.http.TLSConfiguration; -import org.apache.cordova.CallbackContext; - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -39,11 +38,11 @@ abstract class CordovaHttpBase implements Runnable { protected int timeout; protected boolean followRedirects; protected TLSConfiguration tlsConfiguration; - protected CallbackContext callbackContext; + protected CordovaObservableCallbackContext callbackContext; public CordovaHttpBase(String method, String url, String serializer, Object data, JSONObject headers, int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, - CallbackContext callbackContext) { + CordovaObservableCallbackContext callbackContext) { this.method = method; this.url = url; @@ -58,7 +57,7 @@ abstract class CordovaHttpBase implements Runnable { } public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, boolean followRedirects, - String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + String responseType, TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) { this.method = method; this.url = url; @@ -74,8 +73,9 @@ abstract class CordovaHttpBase implements Runnable { public void run() { CordovaHttpResponse response = new CordovaHttpResponse(); + HttpRequest request = null; try { - HttpRequest request = this.createRequest(); + request = this.createRequest(); this.prepareRequest(request); this.sendBody(request); this.processResponse(request, response); @@ -94,10 +94,17 @@ abstract class CordovaHttpBase implements Runnable { response.setErrorMessage("Request timed out: " + e.getMessage()); Log.w(TAG, "Request timed out", e); } else { - response.setStatus(-1); - response.setErrorMessage("There was an error with the request: " + e.getCause().getMessage()); - Log.w(TAG, "Generic request error", e); + String cause = e.getCause().getMessage(); + if(e.getCause() instanceof InterruptedIOException && "thread interrupted".equals(cause.toLowerCase())){ + this.setAborted(request, response); + } else { + response.setStatus(-1); + response.setErrorMessage("There was an error with the request: " + cause); + Log.w(TAG, "Generic request error", e); + } } + } catch (InterruptedException ie) { + this.setAborted(request, response); } catch (Exception e) { response.setStatus(-1); response.setErrorMessage(e.getMessage()); @@ -202,4 +209,17 @@ abstract class CordovaHttpBase implements Runnable { response.setErrorMessage(HttpBodyDecoder.decodeBody(outputStream.toByteArray(), request.charset())); } } + + protected void setAborted(HttpRequest request, CordovaHttpResponse response) { + response.setStatus(-8); + response.setErrorMessage("Request was aborted"); + if(request != null){ + try{ + request.disconnect(); + } catch(Exception any){ + Log.w(TAG, "Failed to close aborted request", any); + } + } + Log.i(TAG, "Request was aborted"); + } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java index d89db82..975f5cc 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java @@ -9,7 +9,6 @@ import javax.net.ssl.SSLSocketFactory; import com.silkimen.http.HttpRequest; import com.silkimen.http.TLSConfiguration; -import org.apache.cordova.CallbackContext; import org.apache.cordova.file.FileUtils; import org.json.JSONObject; @@ -17,7 +16,7 @@ class CordovaHttpDownload extends CordovaHttpBase { private String filePath; public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, boolean followRedirects, - TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) { super("GET", url, headers, timeout, followRedirects, "text", tlsConfiguration, callbackContext); this.filePath = filePath; diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java index 5f17e5d..6eacca5 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java @@ -5,20 +5,19 @@ import javax.net.ssl.SSLSocketFactory; import com.silkimen.http.TLSConfiguration; -import org.apache.cordova.CallbackContext; import org.json.JSONObject; class CordovaHttpOperation extends CordovaHttpBase { public CordovaHttpOperation(String method, String url, String serializer, Object data, JSONObject headers, int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, - CallbackContext callbackContext) { + CordovaObservableCallbackContext callbackContext) { super(method, url, serializer, data, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); } public CordovaHttpOperation(String method, String url, JSONObject headers, int timeout, boolean followRedirects, - String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + String responseType, TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) { super(method, url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index 297dba3..245e4b2 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -1,6 +1,10 @@ package com.silkimen.cordovahttp; import java.security.KeyStore; +import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.Future; import com.silkimen.http.TLSConfiguration; @@ -17,17 +21,22 @@ import android.util.Base64; import javax.net.ssl.TrustManagerFactory; -public class CordovaHttpPlugin extends CordovaPlugin { +public class CordovaHttpPlugin extends CordovaPlugin implements Observer { private static final String TAG = "Cordova-Plugin-HTTP"; private TLSConfiguration tlsConfiguration; + private HashMap> reqMap; + private final Object reqMapLock = new Object(); + @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.tlsConfiguration = new TLSConfiguration(); + this.reqMap = new HashMap>(); + try { KeyStore store = KeyStore.getInstance("AndroidCAStore"); String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); @@ -73,6 +82,8 @@ public class CordovaHttpPlugin extends CordovaPlugin { return this.setServerTrustMode(args, callbackContext); } else if ("setClientAuthMode".equals(action)) { return this.setClientAuthMode(args, callbackContext); + } else if ("abort".equals(action)) { + return this.abort(args, callbackContext); } else { return false; } @@ -87,10 +98,13 @@ public class CordovaHttpPlugin extends CordovaPlugin { boolean followRedirect = args.getBoolean(3); String responseType = args.getString(4); - CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect, - responseType, this.tlsConfiguration, callbackContext); + Integer reqId = args.getInt(5); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(request); + CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect, + responseType, this.tlsConfiguration, observableCallbackContext); + + startRequest(reqId, observableCallbackContext, request); return true; } @@ -106,10 +120,13 @@ public class CordovaHttpPlugin extends CordovaPlugin { boolean followRedirect = args.getBoolean(5); String responseType = args.getString(6); - CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers, - timeout, followRedirect, responseType, this.tlsConfiguration, callbackContext); + Integer reqId = args.getInt(7); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(request); + CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers, + timeout, followRedirect, responseType, this.tlsConfiguration, observableCallbackContext); + + startRequest(reqId, observableCallbackContext, request); return true; } @@ -123,10 +140,13 @@ public class CordovaHttpPlugin extends CordovaPlugin { boolean followRedirect = args.getBoolean(5); String responseType = args.getString(6); - CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect, - responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), callbackContext); + Integer reqId = args.getInt(7); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(upload); + CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect, + responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), observableCallbackContext); + + startRequest(reqId, observableCallbackContext, upload); return true; } @@ -138,14 +158,25 @@ public class CordovaHttpPlugin extends CordovaPlugin { int timeout = args.getInt(3) * 1000; boolean followRedirect = args.getBoolean(4); - CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect, - this.tlsConfiguration, callbackContext); + Integer reqId = args.getInt(5); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(download); + CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect, + this.tlsConfiguration, observableCallbackContext); + + startRequest(reqId, observableCallbackContext, download); return true; } + private void startRequest(Integer reqId, CordovaObservableCallbackContext observableCallbackContext, CordovaHttpBase request) { + synchronized (reqMapLock) { + observableCallbackContext.setObserver(this); + Future task = cordova.getThreadPool().submit(request); + this.addReq(reqId, task, observableCallbackContext); + } + } + private boolean setServerTrustMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException { CordovaServerTrust runnable = new CordovaServerTrust(args.getString(0), this.cordova.getActivity(), this.tlsConfiguration, callbackContext); @@ -166,4 +197,44 @@ public class CordovaHttpPlugin extends CordovaPlugin { return true; } + + private boolean abort(final JSONArray args, final CallbackContext callbackContext) throws JSONException { + + int reqId = args.getInt(0); + boolean result = false; + // NOTE no synchronized (reqMapLock), since even if the req was already removed from reqMap, + // the worst that would happen calling task.cancel(true) is a result of false + // (i.e. same result as locking & not finding the req in reqMap) + Future task = this.reqMap.get(reqId); + if (task != null && !task.isDone()) { + result = task.cancel(true); + } + callbackContext.success(new JSONObject().put("aborted", result)); + + return true; + } + + private void addReq(final Integer reqId, final Future task, final CordovaObservableCallbackContext observableCallbackContext) { + synchronized (reqMapLock) { + if(!task.isDone()){ + this.reqMap.put(reqId, task); + } + } + } + + private void removeReq(final Integer reqId) { + synchronized (reqMapLock) { + this.reqMap.remove(reqId); + } + } + + @Override + public void update(Observable o, Object arg) { + synchronized (reqMapLock) { + CordovaObservableCallbackContext c = (CordovaObservableCallbackContext) arg; + if (c.getCallbackContext().isFinished()) { + removeReq(c.getRequestId()); + } + } + } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java index dcbcd06..a62a98a 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java @@ -17,7 +17,6 @@ import java.net.URI; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; -import org.apache.cordova.CallbackContext; import org.json.JSONArray; import org.json.JSONObject; @@ -28,7 +27,7 @@ class CordovaHttpUpload extends CordovaHttpBase { public CordovaHttpUpload(String url, JSONObject headers, JSONArray filePaths, JSONArray uploadNames, int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, - Context applicationContext, CallbackContext callbackContext) { + Context applicationContext, CordovaObservableCallbackContext callbackContext) { super("POST", url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); this.filePaths = filePaths; diff --git a/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java b/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java new file mode 100644 index 0000000..cff0583 --- /dev/null +++ b/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java @@ -0,0 +1,58 @@ +package com.silkimen.cordovahttp; + +import org.apache.cordova.CallbackContext; +import org.json.JSONObject; + +import java.util.Observer; + +public class CordovaObservableCallbackContext { + + private CallbackContext callbackContext; + private Integer requestId; + private Observer observer; + + public CordovaObservableCallbackContext(CallbackContext callbackContext, Integer requestId) { + this.callbackContext = callbackContext; + this.requestId = requestId; + } + + public void success(JSONObject message) { + this.callbackContext.success(message); + this.notifyObserver(); + } + + public void error(JSONObject message) { + this.callbackContext.error(message); + this.notifyObserver(); + } + + public Integer getRequestId() { + return this.requestId; + } + + public CallbackContext getCallbackContext() { + return callbackContext; + } + + public Observer getObserver() { + return observer; + } + + protected void notifyObserver() { + if(this.observer != null){ + this.observer.update(null, this); + } + } + + /** + * Set an observer that is notified, when {@link #success(JSONObject)} + * or {@link #error(JSONObject)} are called. + * + * NOTE the observer is notified with + *
observer.update(null, cordovaObservableCallbackContext)
+ * @param observer + */ + public void setObserver(Observer observer) { + this.observer = observer; + } +} diff --git a/src/browser/cordova-http-plugin.js b/src/browser/cordova-http-plugin.js index a18eb8a..5a28525 100644 --- a/src/browser/cordova-http-plugin.js +++ b/src/browser/cordova-http-plugin.js @@ -3,6 +3,8 @@ var pluginId = module.id.slice(0, module.id.lastIndexOf('.')); var cordovaProxy = require('cordova/exec/proxy'); var jsUtil = require(pluginId + '.js-util'); +var reqMap = {}; + function serializeJsonData(data) { try { return JSON.stringify(data); @@ -115,6 +117,13 @@ function createXhrFailureObject(xhr) { return obj; } +function injectRequestIdHandler(reqId, cb) { + return function (response) { + delete reqMap[reqId]; + cb(response); + } +} + function getHeaderValue(headers, headerName) { let result = null; @@ -142,7 +151,7 @@ function setHeaders(xhr, headers) { } function sendRequest(method, withData, opts, success, failure) { - var data, serializer, headers, timeout, followRedirect, responseType; + var data, serializer, headers, timeout, followRedirect, responseType, reqId; var url = opts[0]; if (withData) { @@ -152,25 +161,31 @@ function sendRequest(method, withData, opts, success, failure) { timeout = opts[4]; followRedirect = opts[5]; responseType = opts[6]; + reqId = opts[7]; } else { headers = opts[1]; timeout = opts[2]; followRedirect = opts[3]; responseType = opts[4]; - + reqId = opts[5]; } + var onSuccess = injectRequestIdHandler(reqId, success); + var onFail = injectRequestIdHandler(reqId, failure); + var processedData = null; var xhr = new XMLHttpRequest(); + reqMap[reqId] = xhr; + xhr.open(method, url); if (headers.Cookie && headers.Cookie.length > 0) { - return failure('advanced-http: custom cookies not supported on browser platform'); + return onFail('advanced-http: custom cookies not supported on browser platform'); } if (!followRedirect) { - return failure('advanced-http: disabling follow redirect not supported on browser platform'); + return onFail('advanced-http: disabling follow redirect not supported on browser platform'); } switch (serializer) { @@ -179,7 +194,7 @@ function sendRequest(method, withData, opts, success, failure) { processedData = serializeJsonData(data); if (processedData === null) { - return failure('advanced-http: failed serializing data'); + return onFail('advanced-http: failed serializing data'); } break; @@ -218,11 +233,20 @@ function sendRequest(method, withData, opts, success, failure) { setHeaders(xhr, headers); xhr.onerror = function () { - return failure(createXhrFailureObject(xhr)); + return onFail(createXhrFailureObject(xhr)); + }; + + xhr.onabort = function () { + return onFail({ + status: -8, + error: 'Request was aborted', + url: url, + headers: {} + }); }; xhr.ontimeout = function () { - return failure({ + return onFail({ status: -4, error: 'Request timed out', url: url, @@ -234,15 +258,28 @@ function sendRequest(method, withData, opts, success, failure) { if (xhr.readyState !== xhr.DONE) return; if (xhr.status < 200 || xhr.status > 299) { - return failure(createXhrFailureObject(xhr)); + return onFail(createXhrFailureObject(xhr)); } - return success(createXhrSuccessObject(xhr)); + return onSuccess(createXhrSuccessObject(xhr)); }; xhr.send(processedData); } +function abort(opts, success, failure) { + var reqId = opts[0]; + var result = false; + + var xhr = reqMap[reqId]; + if(xhr && xhr.readyState !== xhr.DONE){ + xhr.abort(); + result = true; + } + + success({aborted: result}); +} + var browserInterface = { get: function (success, failure, opts) { return sendRequest('get', false, opts, success, failure); @@ -262,6 +299,9 @@ var browserInterface = { patch: function (success, failure, opts) { return sendRequest('patch', true, opts, success, failure); }, + abort: function (success, failure, opts) { + return abort(opts, success, failure); + }, uploadFile: function (success, failure, opts) { return failure('advanced-http: function "uploadFile" not supported on browser platform'); }, diff --git a/src/ios/CordovaHttpPlugin.h b/src/ios/CordovaHttpPlugin.h index 0e5d867..cce86b4 100644 --- a/src/ios/CordovaHttpPlugin.h +++ b/src/ios/CordovaHttpPlugin.h @@ -15,5 +15,6 @@ - (void)options:(CDVInvokedUrlCommand*)command; - (void)uploadFiles:(CDVInvokedUrlCommand*)command; - (void)downloadFile:(CDVInvokedUrlCommand*)command; +- (void)abort:(CDVInvokedUrlCommand*)command; @end diff --git a/src/ios/CordovaHttpPlugin.m b/src/ios/CordovaHttpPlugin.m index d7f8fcc..482fdcf 100644 --- a/src/ios/CordovaHttpPlugin.m +++ b/src/ios/CordovaHttpPlugin.m @@ -9,6 +9,8 @@ @interface CordovaHttpPlugin() +- (void)addRequest:(NSNumber*)reqId forTask:(NSURLSessionDataTask*)task; +- (void)removeRequest:(NSNumber*)reqId; - (void)setRequestHeaders:(NSDictionary*)headers forManager:(AFHTTPSessionManager*)manager; - (void)handleSuccess:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response andData:(id)data; - (void)handleError:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response error:(NSError*)error; @@ -22,10 +24,20 @@ @implementation CordovaHttpPlugin { AFSecurityPolicy *securityPolicy; NSURLCredential *x509Credential; + NSMutableDictionary *reqDict; } - (void)pluginInitialize { securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; + reqDict = [NSMutableDictionary dictionary]; +} + +- (void)addRequest:(NSNumber*)reqId forTask:(NSURLSessionDataTask*)task { + [reqDict setObject:task forKey:reqId]; +} + +- (void)removeRequest:(NSNumber*)reqId { + [reqDict removeObjectForKey:reqId]; } - (void)setRequestSerializer:(NSString*)serializerName forManager:(AFHTTPSessionManager*)manager { @@ -52,7 +64,7 @@ if (![self->securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) { return NSURLSessionAuthChallengeRejectProtectionSpace; } - + if (credential) { return NSURLSessionAuthChallengeUseCredential; } @@ -62,7 +74,7 @@ *credential = self->x509Credential; return NSURLSessionAuthChallengeUseCredential; } - + return NSURLSessionAuthChallengePerformDefaultHandling; }]; } @@ -111,14 +123,21 @@ } - (void)handleError:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response error:(NSError*)error { + bool aborted = error.code == NSURLErrorCancelled; + if(aborted){ + [dictionary setObject:[NSNumber numberWithInt:-8] forKey:@"status"]; + [dictionary setObject:@"Request was aborted" forKey:@"error"]; + } if (response != nil) { [dictionary setValue:response.URL.absoluteString forKey:@"url"]; - [dictionary setObject:[NSNumber numberWithInt:(int)response.statusCode] forKey:@"status"]; [dictionary setObject:[self copyHeaderFields:response.allHeaderFields] forKey:@"headers"]; - if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) { - [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"]; + if(!aborted){ + [dictionary setObject:[NSNumber numberWithInt:(int)response.statusCode] forKey:@"status"]; + if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) { + [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"]; + } } - } else { + } else if(!aborted) { [dictionary setObject:[self getStatusCode:error] forKey:@"status"]; [dictionary setObject:[error localizedDescription] forKey:@"error"]; } @@ -181,6 +200,7 @@ NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:3] boolValue]; NSString *responseType = [command.arguments objectAtIndex:4]; + NSNumber *reqId = [command.arguments objectAtIndex:5]; [self setRequestSerializer: @"default" forManager: manager]; [self setupAuthChallengeBlock: manager]; @@ -194,30 +214,35 @@ @try { void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; - + // no 'body' for HEAD request, omitting 'data' if ([method isEqualToString:@"HEAD"]) { [self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:nil]; } else { [self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject]; } - + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; }; - + void (^onFailure)(NSURLSessionTask *, NSError *) = ^(NSURLSessionTask *task, NSError *error) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error]; - + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; }; - - [manager downloadTaskWithHTTPMethod:method URLString:url parameters:nil progress:nil success:onSuccess failure:onFailure]; + + NSURLSessionDataTask *task = [manager downloadTaskWithHTTPMethod:method URLString:url parameters:nil progress:nil success:onSuccess failure:onFailure]; + [self addRequest:reqId forTask:task]; } @catch (NSException *exception) { [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; @@ -235,6 +260,7 @@ NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:5] boolValue]; NSString *responseType = [command.arguments objectAtIndex:6]; + NSNumber *reqId = [command.arguments objectAtIndex:7]; [self setRequestSerializer: serializerName forManager: manager]; [self setupAuthChallengeBlock: manager]; @@ -245,30 +271,32 @@ CordovaHttpPlugin* __weak weakSelf = self; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; - + @try { void (^constructBody)(id) = ^(id formData) { NSArray *buffers = [data mutableArrayValueForKey:@"buffers"]; NSArray *fileNames = [data mutableArrayValueForKey:@"fileNames"]; NSArray *names = [data mutableArrayValueForKey:@"names"]; NSArray *types = [data mutableArrayValueForKey:@"types"]; - + NSError *error; - + for (int i = 0; i < [buffers count]; ++i) { NSData *decodedBuffer = [[NSData alloc] initWithBase64EncodedString:[buffers objectAtIndex:i] options:0]; NSString *fileName = [fileNames objectAtIndex:i]; NSString *partName = [names objectAtIndex:i]; NSString *partType = [types objectAtIndex:i]; - + if (![fileName isEqual:[NSNull null]]) { [formData appendPartWithFileData:decodedBuffer name:partName fileName:fileName mimeType:partType]; } else { [formData appendPartWithFormData:decodedBuffer name:[names objectAtIndex:i]]; } } - + if (error) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [dictionary setObject:[NSNumber numberWithInt:400] forKey:@"status"]; [dictionary setObject:@"Could not add part to multipart request body." forKey:@"error"]; @@ -278,30 +306,36 @@ return; } }; - + void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject]; - + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; }; - + void (^onFailure)(NSURLSessionTask *, NSError *) = ^(NSURLSessionTask *task, NSError *error) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error]; - + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; }; - + + NSURLSessionDataTask *task; if ([serializerName isEqualToString:@"multipart"]) { - [manager uploadTaskWithHTTPMethod:method URLString:url parameters:nil constructingBodyWithBlock:constructBody progress:nil success:onSuccess failure:onFailure]; + task = [manager uploadTaskWithHTTPMethod:method URLString:url parameters:nil constructingBodyWithBlock:constructBody progress:nil success:onSuccess failure:onFailure]; } else { - [manager uploadTaskWithHTTPMethod:method URLString:url parameters:data progress:nil success:onSuccess failure:onFailure]; + task = [manager uploadTaskWithHTTPMethod:method URLString:url parameters:data progress:nil success:onSuccess failure:onFailure]; } + [self addRequest:reqId forTask:task]; } @catch (NSException *exception) { [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; @@ -333,27 +367,27 @@ - (void)setClientAuthMode:(CDVInvokedUrlCommand*)command { CDVPluginResult* pluginResult; NSString *mode = [command.arguments objectAtIndex:0]; - + if ([mode isEqualToString:@"none"]) { x509Credential = nil; pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; } - + if ([mode isEqualToString:@"systemstore"]) { NSString *alias = [command.arguments objectAtIndex:1]; - + // TODO - + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"mode 'systemstore' is not supported on iOS"]; } - + if ([mode isEqualToString:@"buffer"]) { CFDataRef container = (__bridge CFDataRef) [command.arguments objectAtIndex:2]; CFStringRef password = (__bridge CFStringRef) [command.arguments objectAtIndex:3]; - + const void *keys[] = { kSecImportExportPassphrase }; const void *values[] = { password }; - + CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL); CFArrayRef items; OSStatus securityError = SecPKCS12Import(container, options, &items); @@ -367,7 +401,7 @@ self->x509Credential = [NSURLCredential credentialWithIdentity:identity certificates: nil persistence:NSURLCredentialPersistenceForSession]; CFRelease(items); - + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; } } @@ -413,6 +447,7 @@ NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:5] boolValue]; NSString *responseType = [command.arguments objectAtIndex:6]; + NSNumber *reqId = [command.arguments objectAtIndex:7]; [self setRequestHeaders: headers forManager: manager]; [self setupAuthChallengeBlock: manager]; @@ -424,7 +459,7 @@ [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { - [manager POST:url parameters:nil constructingBodyWithBlock:^(id formData) { + NSURLSessionDataTask *task = [manager POST:url parameters:nil constructingBodyWithBlock:^(id formData) { NSError *error; for (int i = 0; i < [filePaths count]; i++) { NSString *filePath = (NSString *) [filePaths objectAtIndex:i]; @@ -433,6 +468,8 @@ [formData appendPartWithFileURL:fileURL name:uploadName error:&error]; } if (error) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [dictionary setObject:[NSNumber numberWithInt:500] forKey:@"status"]; [dictionary setObject:@"Could not add file to post body." forKey:@"error"]; @@ -442,6 +479,8 @@ return; } } progress:nil success:^(NSURLSessionTask *task, id responseObject) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject]; @@ -449,6 +488,8 @@ [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; } failure:^(NSURLSessionTask *task, NSError *error) { + [weakSelf removeRequest:reqId]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error]; @@ -456,6 +497,7 @@ [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; }]; + [self addRequest:reqId forTask:task]; } @catch (NSException *exception) { [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; @@ -472,6 +514,7 @@ NSString *filePath = [command.arguments objectAtIndex: 2]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:3] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:4] boolValue]; + NSNumber *reqId = [command.arguments objectAtIndex:5]; [self setRequestHeaders: headers forManager: manager]; [self setupAuthChallengeBlock: manager]; @@ -486,7 +529,8 @@ [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { - [manager GET:url parameters:nil progress: nil success:^(NSURLSessionTask *task, id responseObject) { + NSURLSessionDataTask *task = [manager GET:url parameters:nil progress: nil success:^(NSURLSessionTask *task, id responseObject) { + [weakSelf removeRequest:reqId]; /* * * Licensed to the Apache Software Foundation (ASF) under one @@ -547,6 +591,7 @@ [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; } failure:^(NSURLSessionTask *task, NSError *error) { + [weakSelf removeRequest:reqId]; NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error]; [dictionary setObject:@"There was an error downloading the file" forKey:@"error"]; @@ -555,6 +600,7 @@ [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; }]; + [self addRequest:reqId forTask:task]; } @catch (NSException *exception) { [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; @@ -562,4 +608,32 @@ } } +- (void)abort:(CDVInvokedUrlCommand*)command { + + NSNumber *reqId = [command.arguments objectAtIndex:0]; + + CDVPluginResult *pluginResult; + bool removed = false; + NSURLSessionDataTask *task = [reqDict objectForKey:reqId]; + if(task){ + @try{ + [task cancel]; + removed = true; + } @catch (NSException *exception) { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + [dictionary setValue:exception.userInfo forKey:@"error"]; + [dictionary setObject:[NSNumber numberWithInt:-1] forKey:@"status"]; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + } + + if(!pluginResult){ + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + [dictionary setObject:[NSNumber numberWithBool:removed] forKey:@"aborted"]; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + @end diff --git a/test/e2e-app-template/config.xml b/test/e2e-app-template/config.xml index 92f40b2..8138da6 100644 --- a/test/e2e-app-template/config.xml +++ b/test/e2e-app-template/config.xml @@ -16,6 +16,10 @@ + + + + diff --git a/test/e2e-app-template/network_security_config.xml b/test/e2e-app-template/network_security_config.xml new file mode 100644 index 0000000..d908a81 --- /dev/null +++ b/test/e2e-app-template/network_security_config.xml @@ -0,0 +1,7 @@ + + + + localhost + httpbin.org + + diff --git a/test/e2e-app-template/package.json b/test/e2e-app-template/package.json index a2c6202..50e722f 100644 --- a/test/e2e-app-template/package.json +++ b/test/e2e-app-template/package.json @@ -20,7 +20,12 @@ "platforms": [ "android", "ios" - ] + ], + "plugins": { + "cordova-plugin-device": {} + } }, - "devDependencies": {} -} \ No newline at end of file + "devDependencies": { + "cordova-plugin-device": "2.0.3" + } +} diff --git a/test/e2e-app-template/www/index.js b/test/e2e-app-template/www/index.js index 86d8cd4..016fded 100644 --- a/test/e2e-app-template/www/index.js +++ b/test/e2e-app-template/www/index.js @@ -10,12 +10,12 @@ const app = { var onlyFlaggedTests = []; var enabledTests = []; - + tests.forEach(function (test) { if (test.only) { onlyFlaggedTests.push(test); } - + if (!test.disabled) { enabledTests.push(test); } @@ -50,6 +50,16 @@ const app = { }; }, + skip: function (content) { + document.getElementById('statusInput').value = 'finished'; + app.printResult('result - skipped', content); + + app.lastResult = { + type: 'skipped', + data: content + }; + }, + throw: function (error) { document.getElementById('statusInput').value = 'finished'; app.printResult('result - throwed', error.message); @@ -126,7 +136,7 @@ const app = { const execTest = function () { try { - testDefinition.func(app.resolve, app.reject); + testDefinition.func(app.resolve, app.reject, app.skip); } catch (error) { app.throw(error); } diff --git a/test/e2e-specs.js b/test/e2e-specs.js index 6c210d9..80e175d 100644 --- a/test/e2e-specs.js +++ b/test/e2e-specs.js @@ -83,7 +83,16 @@ const helpers = { } result.type.should.be.equal(expected); - } + }, + isAbortSupported: function () { + if (window.cordova && window.cordova.platformId === 'android') { + var version = device.version; //NOTE will throw error if cordova is present without cordova-plugin-device + var major = parseInt(/^(\d+)(\.|$)/.exec(version)[1], 10); + return isFinite(major) && major >= 6; + } + return true; + }, + getAbortDelay: function () { return 10; }, }; const messageFactory = { @@ -988,6 +997,118 @@ const tests = [ JSON.parse(result.data.data).cookies.should.be.eql({}); } }, + { + description: 'should be able to abort (POST)', + expected: 'rejected: {"status":-8, "error": "Request ...}', + before: helpers.setRawSerializer, + func: function (resolve, reject, skip) { + if (!helpers.isAbortSupported()) { + skip(); + return; + } + helpers.getWithXhr(function (buffer) { + var reqId = cordova.plugin.http.post('http://httpbin.org/anything', buffer, {}, resolve, reject); + + setTimeout(function () { + cordova.plugin.http.abort(reqId); + }, helpers.getAbortDelay()); + + }, './res/cordova_logo.png', 'arraybuffer'); + }, + validationFunc: function (driver, result) { + helpers.checkResult(result, 'rejected'); + result.data.status.should.be.equal(-8); + } + }, + { + description: 'should be able to abort (GET)', + expected: 'rejected: {"status":-8, "error": "Request ...}', + func: function (resolve, reject, skip) { + if (!helpers.isAbortSupported()) { + skip(); + return; + } + var url = 'https://httpbin.org/image/jpeg'; + var options = { method: 'get', responseType: 'blob' }; + var success = function (response) { + resolve({ + isBlob: response.data.constructor === Blob, + type: response.data.type, + byteLength: response.data.size + }); + }; + + var reqId = cordova.plugin.http.sendRequest(url, options, success, reject); + setTimeout(function () { + cordova.plugin.http.abort(reqId); + }, helpers.getAbortDelay()); + }, + validationFunc: function (driver, result) { + helpers.checkResult(result, 'rejected'); + result.data.status.should.be.equal(-8); + } + }, + { + description: 'should be able to abort downloading a file', + expected: 'rejected: {"status":-8, "error": "Request ...}', + func: function (resolve, reject, skip) { + if (!helpers.isAbortSupported()) { + skip(); + return; + } + var sourceUrl = 'http://httpbin.org/xml'; + var targetPath = cordova.file.cacheDirectory + 'test.xml'; + + var reqId = cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function (entry) { + helpers.getWithXhr(function (content) { + resolve({ + sourceUrl: sourceUrl, + targetPath: targetPath, + fullPath: entry.fullPath, + name: entry.name, + content: content + }); + }, targetPath); + }, reject); + + setTimeout(function () { + cordova.plugin.http.abort(reqId); + }, helpers.getAbortDelay()); + + }, + validationFunc: function (driver, result) { + helpers.checkResult(result, 'rejected'); + result.data.status.should.be.equal(-8); + } + }, + { + description: 'should be able to abort uploading a file', + expected: 'rejected: {"status":-8, "error": "Request ...}', + func: function (resolve, reject, skip) { + if (!helpers.isAbortSupported()) { + skip(); + return; + } + var fileName = 'test-file.txt'; + var fileContent = 'I am a dummy file. I am used for testing purposes!'; + var sourcePath = cordova.file.cacheDirectory + fileName; + var targetUrl = 'http://httpbin.org/post'; + + helpers.writeToFile(function () { + + var reqId = cordova.plugin.http.uploadFile(targetUrl, {}, {}, sourcePath, fileName, resolve, reject); + + setTimeout(function () { + cordova.plugin.http.abort(reqId); + }, helpers.getAbortDelay()); + + }, fileName, fileContent); + }, + validationFunc: function (driver, result) { + helpers.checkResult(result, 'rejected'); + result.data.status.should.be.equal(-8); + } + }, ]; if (typeof module !== 'undefined' && module.exports) { diff --git a/test/e2e-tooling/test.js b/test/e2e-tooling/test.js index c34917d..30a7f14 100644 --- a/test/e2e-tooling/test.js +++ b/test/e2e-tooling/test.js @@ -49,12 +49,19 @@ describe('Advanced HTTP e2e test suite', function () { }); const defineTestForMocha = (test, index) => { - it(index + ': ' + test.description, async () => { + it(index + ': ' + test.description, async function () { await clickNext(driver); await validateTestIndex(driver, index); await validateTestTitle(driver, test.description); await waitToBeFinished(driver, test.timeout || 10000); - await validateResult(driver, test.validationFunc, targetInfo); + + const skipped = await checkSkipped(driver); + + if (skipped) { + this.skip(); + } else { + await validateResult(driver, test.validationFunc, targetInfo); + } }); }; @@ -117,6 +124,11 @@ async function validateResult(driver, validationFunc, targetInfo) { validationFunc(driver, result, targetInfo); } +async function checkSkipped(driver) { + const result = await driver.safeExecute('app.lastResult'); + return result.type === 'skipped'; +} + function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/test/js-specs.js b/test/js-specs.js index 10c6236..384a846 100644 --- a/test/js-specs.js +++ b/test/js-specs.js @@ -676,6 +676,20 @@ describe('Common helpers', function () { }) }); }); + + describe('nextRequestId()', function () { + const helpers = require('../www/helpers')(null, null, null, null, null, null); + + it('returns number requestIds', () => { + helpers.nextRequestId().should.be.a('number'); + }); + + it('returns unique requestIds', () => { + const ids = [helpers.nextRequestId(), helpers.nextRequestId(), helpers.nextRequestId()]; + const set = new Set(ids); + ids.should.to.deep.equal(Array.from(set)); + }); + }); }); describe('Dependency Validator', function () { diff --git a/www/error-codes.js b/www/error-codes.js index a0bfd6c..15c13c0 100644 --- a/www/error-codes.js +++ b/www/error-codes.js @@ -6,4 +6,5 @@ module.exports = { UNSUPPORTED_URL: -5, NOT_CONNECTED: -6, POST_PROCESSING_FAILED: -7, + ABORTED: -8, }; diff --git a/www/helpers.js b/www/helpers.js index b88d776..19ad259 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -5,6 +5,13 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64, var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'options', 'upload', 'download']; var validResponseTypes = ['text', 'json', 'arraybuffer', 'blob']; + var nextRequestId = (function(){ + var currReqId = 0; + return function nextRequestId() { + return ++currReqId; + } + })(); + var interface = { b64EncodeUnicode: b64EncodeUnicode, checkClientAuthMode: checkClientAuthMode, @@ -24,6 +31,7 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64, injectCookieHandler: injectCookieHandler, injectFileEntryHandler: injectFileEntryHandler, injectRawResponseHandler: injectRawResponseHandler, + nextRequestId: nextRequestId, }; // expose all functions for testing purposes diff --git a/www/public-interface.js b/www/public-interface.js index 73f75a9..650b5fa 100644 --- a/www/public-interface.js +++ b/www/public-interface.js @@ -26,6 +26,7 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf options: options, uploadFile: uploadFile, downloadFile: downloadFile, + abort: abort, ErrorCode: errorCodes, ponyfills: ponyfills }; @@ -143,23 +144,31 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf var onFail = helpers.injectCookieHandler(url, failure); var onSuccess = helpers.injectCookieHandler(url, helpers.injectRawResponseHandler(options.responseType, success, failure)); + var reqId = helpers.nextRequestId(); + switch (options.method) { case 'post': case 'put': case 'patch': - return helpers.processData(options.data, options.serializer, function (data) { - exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect, options.responseType]); + helpers.processData(options.data, options.serializer, function (data) { + exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect, options.responseType, reqId]); }); + break; case 'upload': var fileOptions = helpers.checkUploadFileOptions(options.filePath, options.name); - return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.timeout, options.followRedirect, options.responseType]); + exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.timeout, options.followRedirect, options.responseType, reqId]); + break; case 'download': var filePath = helpers.checkDownloadFilePath(options.filePath); var onDownloadSuccess = helpers.injectCookieHandler(url, helpers.injectFileEntryHandler(success)); - return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, filePath, options.timeout, options.followRedirect]); + exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, filePath, options.timeout, options.followRedirect, reqId]); + break; default: - return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType]); + exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType, reqId]); + break; } + + return reqId; } function post(url, data, headers, success, failure) { @@ -198,5 +207,9 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf return publicInterface.sendRequest(url, { method: 'download', params: params, headers: headers, filePath: filePath }, success, failure); } + function abort(requestId , success, failure) { + return exec(success, failure, 'CordovaHttpPlugin', 'abort', [requestId]); + } + return publicInterface; }