Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use XDomainRequest in browsers which can't CORS with XMLHttpRequest #55

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules
24 changes: 18 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
var http = module.exports;
var EventEmitter = require('events').EventEmitter;
var Request = require('./lib/request');
var url = require('url')
var url = require('url');

http.request = function (params, cb) {
if (typeof params === 'string') {
Expand All @@ -26,8 +26,7 @@ http.request = function (params, cb) {
params.host = params.host.split(':')[0];
}
if (!params.port) params.port = params.scheme == 'https' ? 443 : 80;

var req = new Request(new xhrHttp, params);
var req = new Request(new (xhrHttp(params)), params);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an appropriate XHR-like thing is now chosen based on the params, not just once at eval time

if (cb) req.on('response', cb);
return req;
};
Expand All @@ -42,10 +41,14 @@ http.get = function (params, cb) {
http.Agent = function () {};
http.Agent.defaultMaxSockets = 4;

var xhrHttp = (function () {
var xhrHttp = function (params) {
if (typeof window === 'undefined') {
throw new Error('no window object present');
}
else if (shouldXDR(params)) {
// NOTE: params.withCredentials will be ignored: http://bit.ly/ie9nocors
return window.XDomainRequest;
}
else if (window.XMLHttpRequest) {
return window.XMLHttpRequest;
}
Expand Down Expand Up @@ -76,7 +79,7 @@ var xhrHttp = (function () {
else {
throw new Error('ajax not supported in this browser');
}
})();
};

http.STATUS_CODES = {
100 : 'Continue',
Expand Down Expand Up @@ -135,4 +138,13 @@ http.STATUS_CODES = {
509 : 'Bandwidth Limit Exceeded',
510 : 'Not Extended', // RFC 2774
511 : 'Network Authentication Required' // RFC 6585
};
};

// whether the request with params should use XDomainRequest
function shouldXDR(params) {
var crossOrigin = params.host != window.location.host.split(':')[0];
var xhrExists = typeof XMLHttpRequest !== 'undefined';
var xdrExists = typeof XDomainRequest !== 'undefined';
var noCORS = xhrExists && ! ('withCredentials' in new XMLHttpRequest);
return crossOrigin && noCORS && xdrExists;
}
48 changes: 45 additions & 3 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var Request = module.exports = function (xhr, params) {
+ (params.port ? ':' + params.port : '')
+ (params.path || '/')
;

if (typeof params.withCredentials === 'undefined') {
params.withCredentials = true;
}
Expand Down Expand Up @@ -53,17 +53,59 @@ var Request = module.exports = function (xhr, params) {
self.emit('close');
});

res.on('ready', function () {
res.once('ready', function () {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ready should def not fire more than once, but this change is a little safety check

self.emit('response', res);
});

xhr.onreadystatechange = function () {
// Fix for IE9 bug
// SCRIPT575: Could not complete the operation due to error c00c023f
// It happens when a request is aborted, calling the success callback anyway with readyState === 4
if (xhr.__aborted) return;
if (xhr.readyState === 4 && xhr.status === 0) {
// onerror will fire and its an error so there should be no res
return;
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that this actually fixes a completely different bug unrelated to XDR.

If node, if you http.request to a bogus host that TCP can't even get to, your callback is never fired and the request fires an error event. I'm pretty sure even in [email protected], your callback would be fired with a response object even though the HTTP connection was never even made.

If needed I can make a separate pull for this.

res.handle(xhr);
};

xhr.onerror = function() {
// http://www.w3.org/TR/XMLHttpRequest/#the-status-attribute
// there was an error making the request
// In node, this means the request should emit 'error' and no response
// should ever be created
if (xhr.status === 0) {
// error flag is set so no response should be gotten
requestError(new Error("XHR is done (state 4) but error flag is set (status 0)"));
return;
}
else if ( ! xhr.status) {
// If falsy but not 0, it's an IE XDR
requestError(new Error('XDomainRequest Error'));
return;
}

xhr.error = xhr.error || 'Unknown error';
res.handle(xhr);

function requestError(err) {
self.emit('error', err);
self.emit('close');
}
};

if (window.XDomainRequest && xhr instanceof window.XDomainRequest) {
// IE XDomainRequest has onload but not onreadystatechange
// Raised when the object has been completely received from the server.
// http://msdn.microsoft.com/en-us/library/ie/ms536942(v=vs.85).aspx
xhr.onload = function() { // IE XDR
res.handle(xhr);
};
// IE sometimes fails if you don't specify every handler.
// https://github.com/matthewwithanm/httpplease.js/blob/861b26a9916543ff34963192d734da4c06603d0b/lib/index.js#L94
xhr.onprogress = function () {};
xhr.ontimeout = function () {};
}
};

inherits(Request, Stream);
Expand Down
25 changes: 22 additions & 3 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,13 @@ Response.prototype.handle = function (res) {
}
}
else if (res.readyState === 4) {
if (!this.statusCode) {
this.statusCode = res.status;
this.emit('ready');
this.statusCode = res.status;
// statusCode 0 means the error flag is set on the request
// and thus there is no response to handle
if (this.statusCode === 0) {
return;
}
this.emit('ready');
this._emitData(res);

if (res.error) {
Expand All @@ -100,6 +103,17 @@ Response.prototype.handle = function (res) {

this.emit('close');
}
else if (isXDomainRequest(res)) {
// XDomainRequest wil only load if it got a 200 statusCode
// http://bugs.jquery.com/ticket/8283
if (!this.statusCode) {
this.statusCode = 200;
}
this.emit('ready');
this._emitData(res);
this.emit('end');
this.emit('close');
}
};

Response.prototype._emitData = function (res) {
Expand All @@ -118,3 +132,8 @@ Response.prototype._emitData = function (res) {
var isArray = Array.isArray || function (xs) {
return Object.prototype.toString.call(xs) === '[object Array]';
};

function isXDomainRequest(res) {
var XDomainRequest = window.XDomainRequest;
return XDomainRequest && res instanceof XDomainRequest;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "http-browserify",
"name": "http-browserify-xdr",
"version": "1.4.1",
"description": "http module compatability for browserify",
"main": "index.js",
Expand Down
31 changes: 21 additions & 10 deletions readme.markdown
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
# http-browserify
# http-browserify-xdr

The
[http](http://nodejs.org/docs/v0.4.10/api/all.html#hTTP) module from node.js,
but for browsers.
[http](http://nodejs.org/docs/v0.4.10/api/all.html#hTTP) module from browserify,
but supporting browsers that can only do cross-origin requests with XDomainRequest (IE8 & IE9).

When you `require('http')` in
[browserify](http://github.com/substack/node-browserify),
this module will be loaded.
The out-of-the-box [http-browserify] does not work for cross-origin requests with XDomainRequest, and [an open issue](https://github.com/substack/http-browserify/pull/3) has gone unresponded to for almost 3 years.

Use this in your browserify project by adding the following to your package.json:
```json
"browser": {
"http": "http-browserify-xdr"
}
```

I intend to have version numbers here mirror http-browserify changes (>= 1.4.1) until @substack merges my pull request.

Note: XDomainRequests [cannot send cookies](http://bit.ly/ie9nocors), so 'withCredentials' options will be ignored. The way to do cross-origin requests withCredentials in these browsers is, well, you can't. You have to open an iframe serving a src on the origin you want to request, then postMessage into it, have it make the request, then postMessage the response out. See [sockjs-client](https://github.com/sockjs/sockjs-client#supported-transports-by-browser-html-served-from-http-or-https) for a referenc eimplementation of that.

The following is the original http-browserify README because the API is the same.

# example

``` js
var http = require('http');

http.get({ path : '/beep' }, function (res) {
http.get({ host: 'anotherorigin.com', path : '/beep' }, function (res) {
var div = document.getElementById('result');
div.innerHTML += 'GET /beep<br>';

Expand Down Expand Up @@ -108,19 +119,19 @@ You can do:

````javascript
var bundle = browserify({
require : { http : 'http-browserify' }
require : { http : 'http-browserify-xdr' }
});
````

in order to map "http-browserify" over `require('http')` in your browserified
in order to map "http-browserify-xdr" over `require('http')` in your browserified
source.

# install

With [npm](https://npmjs.org) do:

```
npm install http-browserify
npm install http-browserify-xdr
```

# license
Expand Down