Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 6680b7b

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 #4987 Closes #6735
1 parent c839f78 commit 6680b7b

File tree

2 files changed

+35
-95
lines changed

2 files changed

+35
-95
lines changed

src/ng/httpBackend.js

+31-28
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

@@ -158,33 +155,39 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
158155
}
159156
};
160157

161-
function jsonpReq(url, done) {
158+
function jsonpReq(url, callbackId, done) {
162159
// we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.:
163160
// - fetches local scripts via XHR and evals them
164161
// - adds and immediately removes script elements from the document
165-
var script = rawDocument.createElement('script'),
166-
doneWrapper = function() {
167-
script.onreadystatechange = script.onload = script.onerror = null;
168-
rawDocument.body.removeChild(script);
169-
if (done) done();
170-
};
171-
172-
script.type = 'text/javascript';
162+
var script = rawDocument.createElement('script'), callback = null;
163+
script.type = "text/javascript";
173164
script.src = url;
174-
175-
if (msie && msie <= 8) {
176-
script.onreadystatechange = function() {
177-
if (/loaded|complete/.test(script.readyState)) {
178-
doneWrapper();
165+
script.async = true;
166+
167+
callback = function(event) {
168+
removeEventListenerFn(script, "load", callback);
169+
removeEventListenerFn(script, "error", callback);
170+
rawDocument.body.removeChild(script);
171+
script = null;
172+
var status = -1;
173+
var text = "unknown";
174+
175+
if (event) {
176+
if (event.type === "load" && !callbacks[callbackId].called) {
177+
event = { type: "error" };
179178
}
180-
};
181-
} else {
182-
script.onload = script.onerror = function() {
183-
doneWrapper();
184-
};
185-
}
179+
text = event.type;
180+
status = event.type === "error" ? 404 : 200;
181+
}
182+
183+
if (done) {
184+
done(status, text);
185+
}
186+
};
186187

188+
addEventListenerFn(script, "load", callback);
189+
addEventListenerFn(script, "error", callback);
187190
rawDocument.body.appendChild(script);
188-
return doneWrapper;
191+
return callback;
189192
}
190193
}

test/ng/httpBackendSpec.js

+4-67
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) {
@@ -325,13 +326,7 @@ describe('$httpBackend', function() {
325326

326327
expect(url[1]).toBe('http://example.org/path');
327328
callbacks[url[2]]('some-data');
328-
329-
if (script.onreadystatechange) {
330-
script.readyState = 'complete';
331-
script.onreadystatechange();
332-
} else {
333-
script.onload();
334-
}
329+
browserTrigger(script, "load");
335330

336331
expect(callback).toHaveBeenCalledOnce();
337332
});
@@ -346,71 +341,13 @@ describe('$httpBackend', function() {
346341
callbackId = script.src.match(SCRIPT_URL)[2];
347342

348343
callbacks[callbackId]('some-data');
349-
350-
if (script.onreadystatechange) {
351-
script.readyState = 'complete';
352-
script.onreadystatechange();
353-
} else {
354-
script.onload();
355-
}
344+
browserTrigger(script, "load");
356345

357346
expect(callbacks[callbackId]).toBe(angular.noop);
358347
expect(fakeDocument.body.removeChild).toHaveBeenCalledOnceWith(script);
359348
});
360349

361350

362-
if(msie<=8) {
363-
364-
it('should attach onreadystatechange handler to the script object', function() {
365-
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop);
366-
367-
expect(fakeDocument.$$scripts[0].onreadystatechange).toEqual(jasmine.any(Function));
368-
369-
var script = fakeDocument.$$scripts[0];
370-
371-
script.readyState = 'complete';
372-
script.onreadystatechange();
373-
374-
expect(script.onreadystatechange).toBe(null);
375-
});
376-
377-
} else {
378-
379-
it('should attach onload and onerror handlers to the script object', function() {
380-
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop);
381-
382-
expect(fakeDocument.$$scripts[0].onload).toEqual(jasmine.any(Function));
383-
expect(fakeDocument.$$scripts[0].onerror).toEqual(jasmine.any(Function));
384-
385-
var script = fakeDocument.$$scripts[0];
386-
script.onload();
387-
388-
expect(script.onload).toBe(null);
389-
expect(script.onerror).toBe(null);
390-
});
391-
392-
}
393-
394-
it('should call callback with status -2 when script fails to load', function() {
395-
callback.andCallFake(function(status, response) {
396-
expect(status).toBe(-2);
397-
expect(response).toBeUndefined();
398-
});
399-
400-
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
401-
expect(fakeDocument.$$scripts.length).toBe(1);
402-
403-
var script = fakeDocument.$$scripts.shift();
404-
if (script.onreadystatechange) {
405-
script.readyState = 'complete';
406-
script.onreadystatechange();
407-
} else {
408-
script.onload();
409-
}
410-
expect(callback).toHaveBeenCalledOnce();
411-
});
412-
413-
414351
it('should set url to current location if not specified or empty string', function() {
415352
$backend('JSONP', undefined, null, callback);
416353
expect(fakeDocument.$$scripts[0].src).toBe($browser.url());

0 commit comments

Comments
 (0)