Skip to content

Commit

Permalink
fix($httpBackend): don't error when JSONP callback called with no par…
Browse files Browse the repository at this point in the history
…ameter

This change brings Angular's JSONP behaviour closer in line with jQuery's. It will no longer treat
a callback called with no data as an error, and will no longer support IE8 via the onreadystatechanged
event.

BREAKING CHANGE:

Previously, the JSONP backend code would support IE8 by relying on the readystatechanged events. This
is no longer the case, as these events do not provide adequate useful information for deeming whether
or not a response is an error.

Previously, a JSONP response which did not pass data into the callback would be given a status of -2,
and treated as an error. Now, this situation will instead be given a status of 200, despite the lack
of data. This is useful for interaction with certain APIs.

Previously, the onload and onerror callbacks were added to the JSONP script tag. These have been
replaced with jQuery events, in order to gain access to the event object. This means that it is now
difficult to test if the callbacks are registered or not. This is possible with jQuery, using the
$.data("events") method, however it is currently impossible with jqLite. This is not expected to
break applications.

Closes angular#4987
Closes angular#6735
  • Loading branch information
caitp committed Apr 7, 2014
1 parent c550c12 commit fcd2545
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 41 deletions.
61 changes: 44 additions & 17 deletions src/ng/httpBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,13 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
var callbackId = '_' + (callbacks.counter++).toString(36);
callbacks[callbackId] = function(data) {
callbacks[callbackId].data = data;
callbacks[callbackId].called = true;
};

var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
function() {
if (callbacks[callbackId].data) {
completeRequest(callback, 200, callbacks[callbackId].data);
} else {
completeRequest(callback, status || -2);
}
callbacks[callbackId] = angular.noop;
callbackId, function(status, text) {
completeRequest(callback, status, callbacks[callbackId].data, "", text);
callbacks[callbackId] = noop;
});
} else {

Expand Down Expand Up @@ -160,33 +157,63 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
};

function jsonpReq(url, done) {
function jsonpReq(url, callbackId, done) {
// we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.:
// - fetches local scripts via XHR and evals them
// - adds and immediately removes script elements from the document
var script = rawDocument.createElement('script'),
doneWrapper = function() {
script.onreadystatechange = script.onload = script.onerror = null;
addedEventListeners,
callback = function(event) {
script.onreadystatechange = null;
if (addedEventListeners) {
removeEventListenerFn(script, "load", callback);
removeEventListenerFn(script, "error", callback);
}
rawDocument.body.removeChild(script);
if (done) done();
script = null;
var status = -1;
var text = "unknown";

if (event) {
if (event.type === "load" && !callbacks[callbackId].called) {
event = { type: "error" };
}
text = event.type;
status = event.type === "error" ? 404 : 200;
} else if (msie <= 8) {
// For IE, we never have an event object here, so the only thing we can do is check if the
// function was called, or not.
if (!callbacks[callbackId].called) {
status = 404;
text = "error";
} else {
status = 200;
text = "success";
}
}

if (done) {
done(status, text);
}
};

script.type = 'text/javascript';
script.type = "text/javascript";
script.src = url;
script.async = true;

if (msie && msie <= 8) {
script.onreadystatechange = function() {
if (/loaded|complete/.test(script.readyState)) {
doneWrapper();
callback();
}
};
} else {
script.onload = script.onerror = function() {
doneWrapper();
};
addedEventListeners = true;
addEventListenerFn(script, "load", callback);
addEventListenerFn(script, "error", callback);
}

rawDocument.body.appendChild(script);
return doneWrapper;
return callback;
}
}
31 changes: 7 additions & 24 deletions test/ng/httpBackendSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ describe('$httpBackend', function() {
fakeDocument = {
$$scripts: [],
createElement: jasmine.createSpy('createElement').andCallFake(function() {
return {};
// Return a proper script element...
return document.createElement(arguments[0]);
}),
body: {
appendChild: jasmine.createSpy('body.appendChild').andCallFake(function(script) {
Expand Down Expand Up @@ -356,7 +357,7 @@ describe('$httpBackend', function() {
script.readyState = 'complete';
script.onreadystatechange();
} else {
script.onload();
browserTrigger(script, "load");
}

expect(callback).toHaveBeenCalledOnce();
Expand All @@ -372,12 +373,11 @@ describe('$httpBackend', function() {
callbackId = script.src.match(SCRIPT_URL)[2];

callbacks[callbackId]('some-data');

if (script.onreadystatechange) {
script.readyState = 'complete';
script.onreadystatechange();
} else {
script.onload();
browserTrigger(script, "load");
}

expect(callbacks[callbackId]).toBe(angular.noop);
Expand All @@ -386,7 +386,6 @@ describe('$httpBackend', function() {


if(msie<=8) {

it('should attach onreadystatechange handler to the script object', function() {
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop);

Expand All @@ -399,27 +398,11 @@ describe('$httpBackend', function() {

expect(script.onreadystatechange).toBe(null);
});

} else {

it('should attach onload and onerror handlers to the script object', function() {
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop);

expect(fakeDocument.$$scripts[0].onload).toEqual(jasmine.any(Function));
expect(fakeDocument.$$scripts[0].onerror).toEqual(jasmine.any(Function));

var script = fakeDocument.$$scripts[0];
script.onload();

expect(script.onload).toBe(null);
expect(script.onerror).toBe(null);
});

}

it('should call callback with status -2 when script fails to load', function() {
it('should call callback with status 404 when script fails to load', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(-2);
expect(status).toBe(404);
expect(response).toBeUndefined();
});

Expand All @@ -431,7 +414,7 @@ describe('$httpBackend', function() {
script.readyState = 'complete';
script.onreadystatechange();
} else {
script.onload();
browserTrigger(script, "load");
}
expect(callback).toHaveBeenCalledOnce();
});
Expand Down

0 comments on commit fcd2545

Please sign in to comment.