Skip to content

Commit fcd2545

Browse files
committed
fix($httpBackend): don't error when JSONP callback called with no parameter
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
1 parent c550c12 commit fcd2545

File tree

2 files changed

+51
-41
lines changed

2 files changed

+51
-41
lines changed

src/ng/httpBackend.js

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,13 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
4949
var callbackId = '_' + (callbacks.counter++).toString(36);
5050
callbacks[callbackId] = function(data) {
5151
callbacks[callbackId].data = data;
52+
callbacks[callbackId].called = true;
5253
};
5354

5455
var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
55-
function() {
56-
if (callbacks[callbackId].data) {
57-
completeRequest(callback, 200, callbacks[callbackId].data);
58-
} else {
59-
completeRequest(callback, status || -2);
60-
}
61-
callbacks[callbackId] = angular.noop;
56+
callbackId, function(status, text) {
57+
completeRequest(callback, status, callbacks[callbackId].data, "", text);
58+
callbacks[callbackId] = noop;
6259
});
6360
} else {
6461

@@ -160,33 +157,63 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
160157
}
161158
};
162159

163-
function jsonpReq(url, done) {
160+
function jsonpReq(url, callbackId, done) {
164161
// we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.:
165162
// - fetches local scripts via XHR and evals them
166163
// - adds and immediately removes script elements from the document
167164
var script = rawDocument.createElement('script'),
168-
doneWrapper = function() {
169-
script.onreadystatechange = script.onload = script.onerror = null;
165+
addedEventListeners,
166+
callback = function(event) {
167+
script.onreadystatechange = null;
168+
if (addedEventListeners) {
169+
removeEventListenerFn(script, "load", callback);
170+
removeEventListenerFn(script, "error", callback);
171+
}
170172
rawDocument.body.removeChild(script);
171-
if (done) done();
173+
script = null;
174+
var status = -1;
175+
var text = "unknown";
176+
177+
if (event) {
178+
if (event.type === "load" && !callbacks[callbackId].called) {
179+
event = { type: "error" };
180+
}
181+
text = event.type;
182+
status = event.type === "error" ? 404 : 200;
183+
} else if (msie <= 8) {
184+
// For IE, we never have an event object here, so the only thing we can do is check if the
185+
// function was called, or not.
186+
if (!callbacks[callbackId].called) {
187+
status = 404;
188+
text = "error";
189+
} else {
190+
status = 200;
191+
text = "success";
192+
}
193+
}
194+
195+
if (done) {
196+
done(status, text);
197+
}
172198
};
173199

174-
script.type = 'text/javascript';
200+
script.type = "text/javascript";
175201
script.src = url;
202+
script.async = true;
176203

177204
if (msie && msie <= 8) {
178205
script.onreadystatechange = function() {
179206
if (/loaded|complete/.test(script.readyState)) {
180-
doneWrapper();
207+
callback();
181208
}
182209
};
183210
} else {
184-
script.onload = script.onerror = function() {
185-
doneWrapper();
186-
};
211+
addedEventListeners = true;
212+
addEventListenerFn(script, "load", callback);
213+
addEventListenerFn(script, "error", callback);
187214
}
188215

189216
rawDocument.body.appendChild(script);
190-
return doneWrapper;
217+
return callback;
191218
}
192219
}

test/ng/httpBackendSpec.js

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ describe('$httpBackend', function() {
3939
fakeDocument = {
4040
$$scripts: [],
4141
createElement: jasmine.createSpy('createElement').andCallFake(function() {
42-
return {};
42+
// Return a proper script element...
43+
return document.createElement(arguments[0]);
4344
}),
4445
body: {
4546
appendChild: jasmine.createSpy('body.appendChild').andCallFake(function(script) {
@@ -356,7 +357,7 @@ describe('$httpBackend', function() {
356357
script.readyState = 'complete';
357358
script.onreadystatechange();
358359
} else {
359-
script.onload();
360+
browserTrigger(script, "load");
360361
}
361362

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

374375
callbacks[callbackId]('some-data');
375-
376376
if (script.onreadystatechange) {
377377
script.readyState = 'complete';
378378
script.onreadystatechange();
379379
} else {
380-
script.onload();
380+
browserTrigger(script, "load");
381381
}
382382

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

387387

388388
if(msie<=8) {
389-
390389
it('should attach onreadystatechange handler to the script object', function() {
391390
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop);
392391

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

400399
expect(script.onreadystatechange).toBe(null);
401400
});
402-
403-
} else {
404-
405-
it('should attach onload and onerror handlers to the script object', function() {
406-
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop);
407-
408-
expect(fakeDocument.$$scripts[0].onload).toEqual(jasmine.any(Function));
409-
expect(fakeDocument.$$scripts[0].onerror).toEqual(jasmine.any(Function));
410-
411-
var script = fakeDocument.$$scripts[0];
412-
script.onload();
413-
414-
expect(script.onload).toBe(null);
415-
expect(script.onerror).toBe(null);
416-
});
417-
418401
}
419402

420-
it('should call callback with status -2 when script fails to load', function() {
403+
it('should call callback with status 404 when script fails to load', function() {
421404
callback.andCallFake(function(status, response) {
422-
expect(status).toBe(-2);
405+
expect(status).toBe(404);
423406
expect(response).toBeUndefined();
424407
});
425408

@@ -431,7 +414,7 @@ describe('$httpBackend', function() {
431414
script.readyState = 'complete';
432415
script.onreadystatechange();
433416
} else {
434-
script.onload();
417+
browserTrigger(script, "load");
435418
}
436419
expect(callback).toHaveBeenCalledOnce();
437420
});

0 commit comments

Comments
 (0)