diff --git a/request/request.js b/request/request.js index b153b0e9..552fa515 100644 --- a/request/request.js +++ b/request/request.js @@ -2,6 +2,8 @@ var buildQueryString = require("../querystring/build") +var FILE_PROTOCOL_REGEX = new RegExp('^file://', 'i') + module.exports = function($window, Promise) { var callbackCount = 0 @@ -53,7 +55,16 @@ module.exports = function($window, Promise) { if (useBody) args.data = args.serialize(args.data) else args.url = assemble(args.url, args.data) - var xhr = new $window.XMLHttpRequest() + var xhr = new $window.XMLHttpRequest(), + aborted = false, + _abort = xhr.abort + + + xhr.abort = function abort() { + aborted = true + _abort.call(xhr) + } + xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) if (args.serialize === JSON.stringify && useBody) { @@ -71,12 +82,13 @@ module.exports = function($window, Promise) { if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr xhr.onreadystatechange = function() { - // Don't throw errors on xhr.abort(). XMLHttpRequests ends up in a state of - // xhr.status == 0 and xhr.readyState == 4 if aborted after open, but before completion. - if (xhr.status && xhr.readyState === 4) { + // Don't throw errors on xhr.abort(). + if(aborted) return + + if (xhr.readyState === 4) { try { var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { + if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { resolve(cast(args.type, response)) } else { diff --git a/request/tests/test-request.js b/request/tests/test-request.js index ade0465c..273d592c 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -390,6 +390,56 @@ o.spec("xhr", function() { o(xhr.getRequestHeader("Accept")).equals("application/json, text/*") } }) + o("doesn't fail on abort", function(done) { + var s = new Date + mock.$defineRoutes({ + "GET /item": function() { + return {status: 200, responseText: JSON.stringify({a: 1})} + } + }) + + var failed = false + var resolved = false + function handleAbort(xhr) { + var onreadystatechange = xhr.onreadystatechange // probably not set yet + var testonreadystatechange = function() { + onreadystatechange.call(xhr) + setTimeout(function() { // allow promises to (not) resolve first + o(failed).equals(false) + o(resolved).equals(false) + done() + }, 0) + } + Object.defineProperty(xhr, 'onreadystatechange', { + set: function(val) { onreadystatechange = val } + , get: function() { return testonreadystatechange } + }) + xhr.abort() + } + xhr({method: "GET", url: "/item", config: handleAbort}).catch(function() { + failed = true + }) + .then(function() { + resolved = true + }) + }) + o("doesn't fail on file:// status 0", function(done) { + var s = new Date + mock.$defineRoutes({ + "GET /item": function() { + return {status: 0, responseText: JSON.stringify({a: 1})} + } + }) + var failed = false + xhr({method: "GET", url: "file:///item"}).catch(function() { + failed = true + }).then(function(data) { + o(failed).equals(false) + o(data).deepEquals({a: 1}) + }).then(function() { + done() + }) + }) /*o("data maintains after interpolate", function() { mock.$defineRoutes({ "PUT /items/:x": function() { @@ -463,5 +513,15 @@ o.spec("xhr", function() { }) }) }) + o("rejects on cors-like error", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 0} + } + }) + xhr({method: "GET", url: "/item"}).catch(function(e) { + o(e instanceof Error).equals(true) + }).then(done) + }) }) }) diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js index 37d48e92..57c2887f 100644 --- a/test-utils/xhrMock.js +++ b/test-utils/xhrMock.js @@ -15,6 +15,7 @@ module.exports = function() { XMLHttpRequest: function XMLHttpRequest() { var args = {} var headers = {} + var aborted = false this.setRequestHeader = function(header, value) { headers[header] = value } @@ -32,11 +33,15 @@ module.exports = function() { } this.send = function(body) { var self = this - var handler = routes[args.method + " " + args.pathname] || serverErrorHandler.bind(null, args.pathname) - var data = handler({url: args.pathname, query: args.search || {}, body: body || null}) + if(!aborted) { + var handler = routes[args.method + " " + args.pathname] || serverErrorHandler.bind(null, args.pathname) + var data = handler({url: args.pathname, query: args.search || {}, body: body || null}) + self.status = data.status + self.responseText = data.responseText + } else { + self.status = 0 + } self.readyState = 4 - self.status = data.status - self.responseText = data.responseText if (args.async === true) { var s = new Date callAsync(function() { @@ -44,6 +49,9 @@ module.exports = function() { }) } } + this.abort = function() { + aborted = true + } }, document: { createElement: function(tag) {