UNPKG

ahr2

Version:

An Abstract Http Request for Node.JS (http/https) and the Browser (XMLHttpRequeuest2). For `npm install ahr2` for Node and `pakmanager build` for Ender / Pakmanager. It should be required as `var request = require('ahr2')`

286 lines (249 loc) 7.8 kB
/*jshint strict:true node:true es5:true onevar:true laxcomma:true laxbreak:true eqeqeq:true immed:true latedef:true*/ /*jslint devel: true, debug: true, es5: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: true */ // This module is meant for modern browsers. Not much abstraction or 1337 majic (function (undefined) { "use strict"; var url //= require('url') , browserJsonpClient = require('./jsonp') , triedHeaders = {} , nativeHttpClient , globalOptions , restricted , window = require('window') , debug = false ; // TODO underExtend localOptions // Restricted Headers // http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method restricted = [ "Accept-Charset" , "Accept-Encoding" , "Connection" , "Content-Length" , "Cookie" , "Cookie2" , "Content-Transfer-Encoding" , "Date" , "Expect" , "Host" , "Keep-Alive" , "Referer" , "TE" , "Trailer" , "Transfer-Encoding" , "Upgrade" , "User-Agent" , "Via" ]; restricted.forEach(function (val, i, arr) { arr[i] = val.toLowerCase(); }); if (!window.XMLHttpRequest) { window.XMLHttpRequest = function() { var ActiveXObject = require('ActiveXObject'); return new ActiveXObject('Microsoft.XMLHTTP'); }; } if (window.XDomainRequest) { // TODO fix IE's XHR/XDR to act as normal XHR2 // check if the location.host is the same (name, port, not protocol) as origin } function encodeData(options, xhr2) { var data , ct = options.overrideResponseType || xhr2.getResponseHeader("content-type") || "" , text , len ; ct = ct.toLowerCase(); if (xhr2.responseType && xhr2.response) { text = xhr2.response; } else { text = xhr2.responseText; } len = text.length; if ('binary' === ct) { if (window.ArrayBuffer && xhr2.response instanceof window.ArrayBuffer) { return xhr2.response; } // TODO how to wrap this for the browser and Node?? if (options.responseEncoder) { return options.responseEncoder(text); } // TODO only Chrome 13 currently handles ArrayBuffers well // imageData could work too // http://synth.bitsnbites.eu/ // http://synth.bitsnbites.eu/play.html // var ui8a = new Uint8Array(data, 0); var i , ui8a = new Array(len) ; for (i = 0; i < text.length; i += 1) { ui8a[i] = (text.charCodeAt(i) & 0xff); } return ui8a; } if (ct.indexOf("xml") >= 0) { return xhr2.responseXML; } if (ct.indexOf("jsonp") >= 0 || ct.indexOf("javascript") >= 0) { console.log("forcing of jsonp not yet supported"); return text; } if (ct.indexOf("json") >= 0) { try { data = JSON.parse(text); } catch(e) { data = text; } return data; } return xhr2.responseText; } function browserHttpClient(req, res) { var options = req.userOptions , xhr2 , xhr2Request , timeoutToken ; function onTimeout() { req.emit("timeout", new Error("timeout after " + options.timeout + "ms")); } function resetTimeout() { clearTimeout(timeoutToken); //timeoutToken = setTimeout(onTimeout, options.timeout); } function sanatizeHeaders(header) { var value = options.headers[header] , headerLc = header.toLowerCase() ; // only warn the user once about bad headers if (-1 !== restricted.indexOf(header.toLowerCase())) { if (!triedHeaders[headerLc]) { console.warn('Ignoring all attempts to set restricted header ' + header + '. See (http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method)'); } triedHeaders[headerLc] = true; return; } try { // throws INVALID_STATE_ERROR if called before `open()` xhr2.setRequestHeader(header, value); } catch(e) { console.error('failed to set header: ' + header); console.error(e); } } // A little confusing that the request object gives you // the response handlers and that the upload gives you // the request handlers, but oh well xhr2 = new window.XMLHttpRequest(); xhr2Request = xhr2.upload; /* Proper States */ xhr2.addEventListener('loadstart', function (ev) { // this fires when the request starts, // but shouldn't fire until the request has loaded // and the response starts req.emit('loadstart', ev); //resetTimeout(); }, true); xhr2.addEventListener('progress', function (ev) { if (!req.loaded) { req.loaded = true; req.emit('progress', {}); req.emit('load', {}); } if (!res.loadstart) { res.headers = xhr2.getAllResponseHeaders(); res.loadstart = true; res.emit('loadstart', ev); } res.emit('progress', ev); //resetTimeout(); }, true); xhr2.addEventListener('load', function (ev) { if (xhr2.status >= 400) { ev.error = new Error(xhr2.status); } ev.target.result = encodeData(options, xhr2); res.emit('load', ev); }, true); /* xhr2Request.addEventListener('loadstart', function (ev) { req.emit('loadstart', ev); //resetTimeout(); }, true); */ xhr2Request.addEventListener('load', function (ev) { //resetTimeout(); req.loaded = true; req.emit('load', ev); res.loadstart = true; res.emit('loadstart', {}); }, true); xhr2Request.addEventListener('progress', function (ev) { resetTimeout(); req.emit('progress', ev); }, true); /* Error States */ xhr2.addEventListener('abort', function (ev) { res.emit('abort', ev); }, true); xhr2Request.addEventListener('abort', function (ev) { req.emit('abort', ev); }, true); xhr2.addEventListener('error', function (ev) { res.emit('error', ev); }, true); xhr2Request.addEventListener('error', function (ev) { req.emit('error', ev); }, true); // the "Request" is what timeouts // the "Response" will timeout as well xhr2.addEventListener('timeout', function (ev) { req.emit('timeout', ev); }, true); xhr2Request.addEventListener('timeout', function (ev) { req.emit('timeout', ev); }, true); /* Cleanup */ res.on('loadend', function () { // loadend is managed by AHR req.status = xhr2.status; res.status = xhr2.status; clearTimeout(timeoutToken); }); if (options.username) { xhr2.open(options.method, options.href, true, options.username, options.password); } else { xhr2.open(options.method, options.href, true); } Object.keys(options.headers).forEach(sanatizeHeaders); setTimeout(function () { if ('binary' === options.overrideResponseType) { xhr2.overrideMimeType("text/plain; charset=x-user-defined"); xhr2.responseType = 'arraybuffer'; } try { xhr2.send(options.encodedBody); } catch(e) { req.emit('error', e); } }, 1); req.abort = function () { xhr2.abort(); }; res.abort = function () { xhr2.abort(); }; res.browserRequest = xhr2; return res; } function send(req, res) { var options = req.userOptions; // TODO fix this ugly hack url = url || require('url'); if (options.jsonp && options.jsonpCallback) { return browserJsonpClient(req, res); } return browserHttpClient(req, res); } module.exports = send; }());