Skip to content

Commit 2ba8659

Browse files
Fix CORS and JSONP on older browsers
1 parent 052ea8f commit 2ba8659

4 files changed

Lines changed: 90 additions & 44 deletions

File tree

lib/resource.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ var extend = require('extend');
1111

1212
var util = require('./util');
1313

14-
// Superagent uses btoa for auth encoding on the client, which is missing from <IE10
14+
// SuperAgent uses btoa for auth encoding on the client, which is missing from <IE10
1515
// https://github.com/visionmedia/superagent#supported-browsers
1616
if (!util.isNode && !window.btoa) window.btoa = require('./base64').encode;
1717

@@ -166,18 +166,36 @@ var jsonpRequest = function(method, data, callback) {
166166
script.src = url;
167167

168168
var timer;
169-
var jsonpCallback = _.once(function(data) {
170-
document.getElementsByTagName('head')[0].removeChild(script);
171-
if (data instanceof Error) return callback(data);
172-
if (timer) clearTimeout(timer);
173-
processResponse({status: 200}, data, callback);
174-
});
169+
var first = true;
170+
171+
window[callbackName] = function(data) {
172+
var timedout = data instanceof Error;
173+
174+
// Cancel timeout
175+
if (timer) {
176+
clearTimeout(timer);
177+
timer = null;
178+
}
175179

176-
window[callbackName] = jsonpCallback;
180+
// Do not cleanup if there was a timeout, in case the real request ever comes back
181+
if (!timedout) {
182+
document.getElementsByTagName('head')[0].removeChild(script);
183+
delete window[callbackName];
184+
}
185+
186+
// Only run user callback once
187+
if (first) {
188+
first = false;
189+
timedout ? callback(data) : processResponse({status: 200}, data, callback);
190+
}
191+
};
177192

178193
if (self.options.timeout) {
179194
timer = setTimeout(function() {
180-
jsonpCallback(new Error('timeout of ' + self.options.timeout + 'ms exceeded'));
195+
timer = null;
196+
var err = new Error('timeout of ' + self.options.timeout + 'ms exceeded');
197+
err.callbackName = callbackName; // For testing purposes
198+
window[callbackName](err);
181199
}, self.options.timeout);
182200
}
183201

@@ -233,12 +251,20 @@ var superAgentRequest = function(request, callback) {
233251
// http://visionmedia.github.io/superagent/#buffering-responses
234252
if (request.buffer) request.buffer();
235253

236-
request.end(function(err, res) {
237-
// Superagent considers statuses <200 and >=300 as errors
238-
// http://visionmedia.github.io/superagent/#error-handling
239-
if (err) return callback(err, res);
240-
processResponse(res, null, callback);
241-
});
254+
// Not all weird browser errors are catched by SuperAgent
255+
try {
256+
request.end(function(err, res) {
257+
// In case the request is not async, we don't want to catch the callback exceptions
258+
setTimeout(function() {
259+
// SuperAgent considers statuses <200 and >=300 as errors
260+
// http://visionmedia.github.io/superagent/#error-handling
261+
if (err) return callback(err, res);
262+
processResponse(res, null, callback);
263+
}, 0);
264+
});
265+
} catch (err) {
266+
callback(err);
267+
}
242268
}
243269

244270
var processResponse = function(res, data, callback) {

lib/util.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module.exports.isNode = !(typeof window !== 'undefined' && window !== null);
22

3-
module.exports.supportsCORS = !module.exports.isNode && (('withCredentials' in new XMLHttpRequest()) || (typeof XDomainRequest !== 'undefined'));
3+
module.exports.supportsCORS = !module.exports.isNode && (typeof XMLHttpRequest !== 'undefined') && ('withCredentials' in new XMLHttpRequest());

test/config.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var fs = require('fs');
22
var zlib = require('zlib');
3+
var url = require('url');
34

45
module.exports.port = 8081;
56
module.exports.host = 'http://localhost:' + module.exports.port;
@@ -18,20 +19,22 @@ module.exports.routes = function (req, res) {
1819
}
1920

2021
var success = function(data) {
21-
data || (data = {url: req.url});
22+
data || (data = {url: req.url.replace(/solidus_client_jsonp_callback_\d+/, 'solidus_client_jsonp_callback')});
2223
res.writeHead(200, {'Content-Type': 'application/json'});
23-
if (req.url.indexOf('solidus_client_jsonp_callback_100000') == -1) {
24-
res.end(JSON.stringify(data));
24+
var callback = url.parse(req.url, true).query.callback;
25+
if (callback) {
26+
res.end(callback + '(' + JSON.stringify(data) + ');');
2527
} else {
26-
res.end('solidus_client_jsonp_callback_100000(' + JSON.stringify(data) + ');');
28+
res.end(JSON.stringify(data));
2729
}
2830
};
31+
2932
var not_found = function() {
3033
res.writeHead(404, {'Content-Type': 'application/json'});
3134
res.end(JSON.stringify({error: 'Not Found: ' + req.url}));
3235
};
3336

34-
switch (req.method + ' ' + req.url) {
37+
switch ((req.method + ' ' + req.url).replace(/solidus_client_jsonp_callback_\d+/, 'solidus_client_jsonp_callback')) {
3538
case 'GET /page':
3639
case 'GET /page?a=':
3740
case 'GET /page?a=1':
@@ -42,10 +45,10 @@ module.exports.routes = function (req, res) {
4245
case 'GET /page?a=1&b=2&c%5B0%5D=3&c%5B1%5D=4&d=%7B%22e%22%3A5%2C%22f%22%3A%5B6%2C7%5D%7D':
4346
case 'GET /api/resource.json?url=http%3A%2F%2Fsolidus.com%3Fa%3D1':
4447
case 'GET /custom/route/resource.json?url=http%3A%2F%2Fsolidus.com':
45-
case 'GET /page?a=1&b=2&callback=solidus_client_jsonp_callback_100000':
46-
case 'GET /with-post-object?a=1&b=2&callback=solidus_client_jsonp_callback_100000&id=1&type=object':
47-
case 'GET /with-post-string?a=1&b=2&callback=solidus_client_jsonp_callback_100000&id=1&type=string':
48-
case 'GET /with-post-empty?a=1&b=2&callback=solidus_client_jsonp_callback_100000':
48+
case 'GET /page?a=1&b=2&callback=solidus_client_jsonp_callback':
49+
case 'GET /with-post-object?a=1&b=2&callback=solidus_client_jsonp_callback&id=1&type=object':
50+
case 'GET /with-post-string?a=1&b=2&callback=solidus_client_jsonp_callback&id=1&type=string':
51+
case 'GET /with-post-empty?a=1&b=2&callback=solidus_client_jsonp_callback':
4952
success();
5053
break;
5154

@@ -74,7 +77,7 @@ module.exports.routes = function (req, res) {
7477

7578
case 'GET /with-delay':
7679
case 'GET /api/resource.json?url=http%3A%2F%2Fsolidus.com%2Fwith-delay':
77-
case 'GET /with-delay?callback=solidus_client_jsonp_callback_100000':
80+
case 'GET /with-delay?callback=solidus_client_jsonp_callback':
7881
setTimeout(success, 100);
7982
break;
8083

test/tests/resource.js

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,6 @@ var host = require('../config').host;
77
module.exports = function() {
88

99
describe('Resource', function() {
10-
var random = Math.random;
11-
12-
before(function() {
13-
Math.random = function() {return 1};
14-
})
15-
16-
after(function() {
17-
Math.random = random;
18-
});
19-
2010
describe('.constructor', function() {
2111
it('initializes url, dynamic and options', function(done) {
2212
var resource = new Resource('http://solidus.com/page');
@@ -313,7 +303,7 @@ describe('Resource', function() {
313303
if (util.isNode || util.supportsCORS) {
314304
assert.deepEqual(res.data, {url: '/page?a=1&b=2'});
315305
} else {
316-
assert.deepEqual(res.data, {url: '/page?a=1&b=2&callback=solidus_client_jsonp_callback_100000'});
306+
assert.deepEqual(res.data, {url: '/page?a=1&b=2&callback=solidus_client_jsonp_callback'});
317307
}
318308
done();
319309
});
@@ -369,19 +359,46 @@ describe('Resource', function() {
369359

370360
describe('with jsonp', function() {
371361
it('with successful response', function(done) {
372-
var resource = new Resource({url: 'http://localhost:8081/page?a=1', query: {b: 2}, jsonp: true});
362+
var resource = new Resource({url: host + '/page?a=1', query: {b: 2}, jsonp: true});
373363
resource.get(function(err, res) {
374364
assert.equal(err, null);
375-
assert.deepEqual(res.data, {url: '/page?a=1&b=2&callback=solidus_client_jsonp_callback_100000'});
365+
assert.deepEqual(res.data, {url: '/page?a=1&b=2&callback=solidus_client_jsonp_callback'});
376366
done();
377367
});
378368
});
379369

380370
it('with timeout', function(done) {
381-
var resource = new Resource({url: 'http://localhost:8081/with-delay', jsonp: true, timeout: 1});
371+
var called;
372+
var resource = new Resource({url: host + '/with-delay', jsonp: true, timeout: 1});
382373
resource.get(function(err, res) {
383374
assert.equal(err.message, 'timeout of 1ms exceeded');
384-
assert(!res.data)
375+
assert(!res.data);
376+
assert(!called);
377+
called = true;
378+
379+
// Highjack the jsonp callback, to end the test when the real request come back
380+
var oldCallback = window[err.callbackName];
381+
window[err.callbackName] = function(data) {
382+
assert.deepEqual(data, {url: '/with-delay?callback=solidus_client_jsonp_callback'});
383+
oldCallback(data);
384+
assert(!window[err.callbackName]);
385+
done();
386+
}
387+
});
388+
});
389+
390+
it('with connection error', function(done) {
391+
// If this test fails, maybe you have something running on port 8888?
392+
var called;
393+
var resource = new Resource({url: 'http://localhost:8888', jsonp: true, timeout: 1});
394+
resource.get(function(err, res) {
395+
assert.equal(err.message, 'timeout of 1ms exceeded');
396+
assert(!res.data);
397+
assert(!called);
398+
called = true;
399+
400+
// The real request will never come back
401+
window[err.callbackName]({});
385402
done();
386403
});
387404
});
@@ -434,7 +451,7 @@ describe('Resource', function() {
434451
var resource = new Resource({url: host + '/with-post-object?a=1', query: {b: 2}, jsonp: true});
435452
resource.post({id: 1, type: 'object'}, function(err, res) {
436453
assert.equal(err, null);
437-
assert.deepEqual(res.data, {url: '/with-post-object?a=1&b=2&callback=solidus_client_jsonp_callback_100000&id=1&type=object'});
454+
assert.deepEqual(res.data, {url: '/with-post-object?a=1&b=2&callback=solidus_client_jsonp_callback&id=1&type=object'});
438455
done();
439456
});
440457
});
@@ -443,7 +460,7 @@ describe('Resource', function() {
443460
var resource = new Resource({url: host + '/with-post-string?a=1', query: {b: 2}, jsonp: true});
444461
resource.post('id=1&type=string', function(err, res) {
445462
assert.equal(err, null);
446-
assert.deepEqual(res.data, {url: '/with-post-string?a=1&b=2&callback=solidus_client_jsonp_callback_100000&id=1&type=string'});
463+
assert.deepEqual(res.data, {url: '/with-post-string?a=1&b=2&callback=solidus_client_jsonp_callback&id=1&type=string'});
447464
done();
448465
});
449466
});
@@ -452,7 +469,7 @@ describe('Resource', function() {
452469
var resource = new Resource({url: host + '/with-post-empty?a=1', query: {b: 2}, jsonp: true});
453470
resource.post(null, function(err, res) {
454471
assert.equal(err, null);
455-
assert.deepEqual(res.data, {url: '/with-post-empty?a=1&b=2&callback=solidus_client_jsonp_callback_100000'});
472+
assert.deepEqual(res.data, {url: '/with-post-empty?a=1&b=2&callback=solidus_client_jsonp_callback'});
456473
done();
457474
});
458475
});

0 commit comments

Comments
 (0)