Reqwest is a general purpose XHR (AJAX) connection manager that performs asynchronous http requests in the browser.
/*!
* Reqwest! A general purpose XHR connection manager
* license MIT (c) Dustin Diaz 2015
* https://github.com/ded/reqwest
*/
///////////////////////////////////////////////////////////////////////////////
// All over again. Includes support for xmlHttpRequest, JSONP, CORS, and
// CommonJS Promises A.
//
// It is also isomorphic allowing you to require('reqwest') in Node.js through
// the peer dependency xhr2, albeit the original intent of this library is for
// the browser. For a more thorough solution for Node.js, see mikeal/request.
//
// ## API
//
// reqwest("path/to/html", function(resp) {
// qwery("#content").html(resp);
// });
//
// reqwest({
// url: "path/to/html",
// method: "post",
// data: { foo: "bar", baz: 100 },
// success: function(resp) {
// qwery("#content").html(resp);
// }
// });
//
// reqwest({
// url: "path/to/html",
// method: "get",
// data: [{ name: "foo", value: "bar" }, { name: "baz", value: 100 }],
// success: function(resp) {
// qwery("#content").html(resp);
// }
// });
//
// reqwest({
// url: "path/to/json",
// type: "json",
// method: "post",
// error: function(err) {},
// success: function(resp) {
// qwery("#content").html(resp.content);
// }
// });
//
// reqwest({
// url: "path/to/json",
// type: "json",
// method: "post",
// contentType: "application/json",
// headers: {
// "X-My-Custom-Header": "SomethingImportant"
// },
// error: function(err) {},
// success: function(resp) {
// qwery("#content").html(resp.content);
// }
// });
//
// Uses XMLHttpRequest2 credentialled requests (cookies, HTTP basic auth) if
// supported:
//
// reqwest({
// url: "path/to/json",
// type: "json",
// method: "post",
// contentType: "application/json",
// crossOrigin: true,
// withCredentials: true,
// error: function(err) {},
// success: function(resp) {
// qwery("#content").html(resp.content);
// }
// });
//
// reqwest({
// url: "path/to/data.jsonp?callback=?",
// type: "jsonp",
// success: function(resp) {
// qwery("#content").html(resp.content);
// }
// });
//
// reqwest({
// url: "path/to/data.jsonp?foo=bar",
// type: "jsonp",
// jsonpCallback: "foo",
// jsonpCallbackName: "bar",
// success: function(resp) {
// qwery("#content").html(resp.content);
// }
// });
//
// reqwest({
// url: "path/to/data.jsonp?foo=bar",
// type: "jsonp",
// jsonpCallback: "foo",
// success: function(resp) {
// qwery("#content").html(resp.content);
// },
// complete: function(resp) {
// qwery("#hide-this").hide();
// }
// });
//
// ## Promises
//
// reqwest({
// url: "path/to/data.jsonp?foo=bar",
// type: "jsonp",
// jsonpCallback: "foo"
// })
// .then(
// function(resp) {
// qwery("#content").html(resp.content);
// },
// function(err, msg) {
// qwery("#errors").html(msg);
// }
// )
// .always(function(resp) {
// qwery("#hide-this").hide();
// });
//
// reqwest({
// url: "path/to/data.jsonp?foo=bar",
// type: "jsonp",
// jsonpCallback: "foo"
// })
// .then(function(resp) {
// qwery("#content").html(resp.content);
// })
// .fail(function(err, msg) {
// qwery("#errors").html(msg);
// })
// .always(function(resp) {
// qwery("#hide-this").hide();
// });
//
// var r = reqwest({
// url: "path/to/data.jsonp?foo=bar",
// type: "jsonp",
// jsonpCallback: "foo",
// success: function() {
// setTimeout(function() {
// r.then(
// function(resp) {
// qwery("#content").html(resp.content);
// },
// function(err) {}
// ).always(function(resp) {
// qwery("#hide-this").hide();
// });
// }, 15);
// }
// });
//
// ## Options
//
// * `url` a fully qualified uri
// * `method` http method (default: `GET`)
// * `headers` http headers (default: `{}`)
// * `data` entity body for `PATCH`, `POST` and `PUT` requests. Must be a query `String` or `JSON` object
// * `type` a string enum. `html`, `xml`, `json`, or `jsonp`. Default is inferred by resource extension. Eg: `.json` will set `type` to `json`. `.xml` to `xml` etc.
// * `contentType` sets the `Content-Type` of the request. Eg: `application/json`
// * `crossOrigin` for cross-origin requests for browsers that support this feature.
// * `success` A function called when the request successfully completes
// * `error` A function called when the request fails.
// * `complete` A function called whether the request is a success or failure. Always called when complete.
// * `jsonpCallback` Specify the callback function name for a `JSONP` request. This value will be used instead of the random (but recommended) name automatically generated by reqwest.
//
// ## Security
//
// If you are *still* requiring support for IE6/IE7, consider
// including [JSON3](https://bestiejs.github.io/json3/) in your project.
// Or simply do the following
//
// <script>
// (function() {
// if (!window.JSON) {
// document.write('<scr' + 'ipt src="http://cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.min.js"><\/scr' + 'ipt>')
// }
// }());
// </script>
//
///////////////////////////////////////////////////////////////////////////////
!(function(name, context, definition) {
if (typeof module != "undefined" && module.exports)
module.exports = definition();
else if (typeof define == "function" && define.amd) define(definition);
else context[name] = definition();
})("reqwest", this, function() {
var context = this;
if ("window" in context) {
var doc = document,
byTag = "getElementsByTagName",
head = doc[byTag]("head")[0];
} else {
var XHR2;
try {
XHR2 = require("xhr2");
} catch (ex) {
throw new Error(
"Peer dependency `xhr2` required! Please npm install xhr2"
);
}
}
var httpsRe = /^http/,
protocolRe = /(^\w+):\/\//,
twoHundo = /^(20\d|1223)$/, //http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request
readyState = "readyState",
contentType = "Content-Type",
requestedWith = "X-Requested-With",
uniqid = 0,
callbackPrefix = "reqwest_" + +new Date(),
lastValue, // data stored by the most recent JSONP callback
xmlHttpRequest = "XMLHttpRequest",
xDomainRequest = "XDomainRequest",
noop = function() {},
isArray =
typeof Array.isArray == "function"
? Array.isArray
: function(a) {
return a instanceof Array;
},
defaultHeaders = {
contentType: "application/x-www-form-urlencoded",
requestedWith: xmlHttpRequest,
accept: {
"*":
"text/javascript, text/html, application/xml, text/xml, */*",
xml: "application/xml, text/xml",
html: "text/html",
text: "text/plain",
json: "application/json, text/javascript",
js: "application/javascript, text/javascript"
}
},
xhr = function(o) {
// is it x-domain
if (o["crossOrigin"] === true) {
var xhr = context[xmlHttpRequest] ? new XMLHttpRequest() : null;
if (xhr && "withCredentials" in xhr) {
return xhr;
} else if (context[xDomainRequest]) {
return new XDomainRequest();
} else {
throw new Error(
"Browser does not support cross-origin requests"
);
}
} else if (context[xmlHttpRequest]) {
return new XMLHttpRequest();
} else if (XHR2) {
return new XHR2();
} else {
return new ActiveXObject("Microsoft.XMLHTTP");
}
},
globalSetupOptions = {
dataFilter: function(data) {
return data;
}
};
function succeed(r) {
var protocol = protocolRe.exec(r.url);
protocol = (protocol && protocol[1]) || context.location.protocol;
return httpsRe.test(protocol)
? twoHundo.test(r.request.status)
: !!r.request.response;
}
function handleReadyState(r, success, error) {
return function() {
// use _aborted to mitigate against IE err c00c023f
// (can't read props on aborted request objects)
if (r._aborted) return error(r.request);
if (r._timedOut)
return error(r.request, "Request is aborted: timeout");
if (r.request && r.request[readyState] == 4) {
r.request.onreadystatechange = noop;
if (succeed(r)) success(r.request);
else error(r.request);
}
};
}
function setHeaders(http, o) {
var headers = o["headers"] || {},
h;
headers["Accept"] =
headers["Accept"] ||
defaultHeaders["accept"][o["type"]] ||
defaultHeaders["accept"]["*"];
var isAFormData =
typeof FormData !== "undefined" && o["data"] instanceof FormData;
// breaks cross-origin requests with legacy browsers
if (!o["crossOrigin"] && !headers[requestedWith])
headers[requestedWith] = defaultHeaders["requestedWith"];
if (!headers[contentType] && !isAFormData)
headers[contentType] =
o["contentType"] || defaultHeaders["contentType"];
for (h in headers)
headers.hasOwnProperty(h) &&
"setRequestHeader" in http &&
http.setRequestHeader(h, headers[h]);
}
function setCredentials(http, o) {
if (
typeof o["withCredentials"] !== "undefined" &&
typeof http.withCredentials !== "undefined"
) {
http.withCredentials = !!o["withCredentials"];
}
}
function generalCallback(data) {
lastValue = data;
}
function urlappend(url, s) {
return url + (/\?/.test(url) ? "&" : "?") + s;
}
function handleJsonp(o, fn, err, url) {
var reqId = uniqid++,
cbkey = o["jsonpCallback"] || "callback", // the 'callback' key
cbval = o["jsonpCallbackName"] || reqwest.getcallbackPrefix(reqId),
cbreg = new RegExp("((^|\\?|&)" + cbkey + ")=([^&]+)"),
match = url.match(cbreg),
script = doc.createElement("script"),
loaded = 0,
isIE10 = navigator.userAgent.indexOf("MSIE 10.0") !== -1;
if (match) {
if (match[3] === "?") {
url = url.replace(cbreg, "$1=" + cbval); // wildcard callback func name
} else {
cbval = match[3]; // provided callback func name
}
} else {
url = urlappend(url, cbkey + "=" + cbval); // no callback details, add 'em
}
context[cbval] = generalCallback;
script.type = "text/javascript";
script.src = url;
script.async = true;
if (typeof script.onreadystatechange !== "undefined" && !isIE10) {
// need this for IE due to out-of-order onreadystatechange(), binding script
// execution to an event listener gives us control over when the script
// is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
script.htmlFor = script.id = "_reqwest_" + reqId;
}
script.onload = script.onreadystatechange = function() {
if (
(script[readyState] &&
script[readyState] !== "complete" &&
script[readyState] !== "loaded") ||
loaded
) {
return false;
}
script.onload = script.onreadystatechange = null;
script.onclick && script.onclick();
// Call the user callback with the last value stored and clean up values and scripts.
fn(lastValue);
lastValue = undefined;
head.removeChild(script);
loaded = 1;
};
// Add the script to the DOM head
head.appendChild(script);
// Enable JSONP timeout
return {
abort: function() {
script.onload = script.onreadystatechange = null;
err({}, "Request is aborted: timeout", {});
lastValue = undefined;
head.removeChild(script);
loaded = 1;
}
};
}
function getRequest(fn, err) {
var o = this.o,
method = (o["method"] || "GET").toUpperCase(),
url = typeof o === "string" ? o : o["url"],
// convert non-string objects to query-string form unless o['processData'] is false
data =
o["processData"] !== false &&
o["data"] &&
typeof o["data"] !== "string"
? reqwest.toQueryString(o["data"])
: o["data"] || null,
http,
sendWait = false;
// if we're working on a GET request and we have data then we should append
// query string to end of URL and not post data
if ((o["type"] == "jsonp" || method == "GET") && data) {
url = urlappend(url, data);
data = null;
}
if (o["type"] == "jsonp") return handleJsonp(o, fn, err, url);
// get the xhr from the factory if passed
// if the factory returns null, fall-back to ours
http = (o.xhr && o.xhr(o)) || xhr(o);
http.open(method, url, o["async"] === false ? false : true);
setHeaders(http, o);
setCredentials(http, o);
if (
context[xDomainRequest] &&
http instanceof context[xDomainRequest]
) {
http.onload = fn;
http.onerror = err;
// NOTE: see
// http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/30ef3add-767c-4436-b8a9-f1ca19b4812e
http.onprogress = function() {};
sendWait = true;
} else {
http.onreadystatechange = handleReadyState(this, fn, err);
}
o["before"] && o["before"](http);
if (sendWait) {
setTimeout(function() {
http.send(data);
}, 200);
} else {
http.send(data);
}
return http;
}
function Reqwest(o, fn) {
this.o = o;
this.fn = fn;
init.apply(this, arguments);
}
function setType(header) {
// json, javascript, text/plain, text/html, xml
if (header === null) return undefined; //In case of no content-type.
if (header.match("json")) return "json";
if (header.match("javascript")) return "js";
if (header.match("text")) return "html";
if (header.match("xml")) return "xml";
}
function init(o, fn) {
this.url = typeof o == "string" ? o : o["url"];
this.timeout = null;
// whether request has been fulfilled for purpose
// of tracking the Promises
this._fulfilled = false;
// success handlers
this._successHandler = function() {};
this._fulfillmentHandlers = [];
// error handlers
this._errorHandlers = [];
// complete (both success and fail) handlers
this._completeHandlers = [];
this._erred = false;
this._responseArgs = {};
var self = this;
fn = fn || function() {};
if (o["timeout"]) {
this.timeout = setTimeout(function() {
timedOut();
}, o["timeout"]);
}
if (o["success"]) {
this._successHandler = function() {
o["success"].apply(o, arguments);
};
}
if (o["error"]) {
this._errorHandlers.push(function() {
o["error"].apply(o, arguments);
});
}
if (o["complete"]) {
this._completeHandlers.push(function() {
o["complete"].apply(o, arguments);
});
}
function complete(resp) {
o["timeout"] && clearTimeout(self.timeout);
self.timeout = null;
while (self._completeHandlers.length > 0) {
self._completeHandlers.shift()(resp);
}
}
function success(resp) {
var type =
o["type"] ||
(resp && setType(resp.getResponseHeader("Content-Type"))); // resp can be undefined in IE
resp = type !== "jsonp" ? self.request : resp;
// use global data filter on response text
var filteredResponse = globalSetupOptions.dataFilter(
resp.responseText,
type
),
r = filteredResponse;
try {
resp.responseText = r;
} catch (e) {
// can't assign this in IE<=8, just ignore
}
if (r) {
switch (type) {
case "json":
try {
resp = context.JSON
? context.JSON.parse(r)
: eval("(" + r + ")");
} catch (err) {
return error(
resp,
"Could not parse JSON in response",
err
);
}
break;
case "js":
resp = eval(r);
break;
case "html":
resp = r;
break;
case "xml":
resp =
resp.responseXML &&
resp.responseXML.parseError && // IE trololo
resp.responseXML.parseError.errorCode &&
resp.responseXML.parseError.reason
? null
: resp.responseXML;
break;
}
}
self._responseArgs.resp = resp;
self._fulfilled = true;
fn(resp);
self._successHandler(resp);
while (self._fulfillmentHandlers.length > 0) {
resp = self._fulfillmentHandlers.shift()(resp);
}
complete(resp);
}
function timedOut() {
self._timedOut = true;
self.request.abort();
}
function error(resp, msg, t) {
resp = self.request;
self._responseArgs.resp = resp;
self._responseArgs.msg = msg;
self._responseArgs.t = t;
self._erred = true;
while (self._errorHandlers.length > 0) {
self._errorHandlers.shift()(resp, msg, t);
}
complete(resp);
}
this.request = getRequest.call(this, success, error);
}
Reqwest.prototype = {
abort: function() {
this._aborted = true;
this.request.abort();
},
retry: function() {
init.call(this, this.o, this.fn);
},
/**
* Small deviation from the Promises A CommonJs specification
* http://wiki.commonjs.org/wiki/Promises/A
*/
/**
* `then` will execute upon successful requests
*/
then: function(success, fail) {
success = success || function() {};
fail = fail || function() {};
if (this._fulfilled) {
this._responseArgs.resp = success(this._responseArgs.resp);
} else if (this._erred) {
fail(
this._responseArgs.resp,
this._responseArgs.msg,
this._responseArgs.t
);
} else {
this._fulfillmentHandlers.push(success);
this._errorHandlers.push(fail);
}
return this;
},
/**
* `always` will execute whether the request succeeds or fails
*/
always: function(fn) {
if (this._fulfilled || this._erred) {
fn(this._responseArgs.resp);
} else {
this._completeHandlers.push(fn);
}
return this;
},
/**
* `fail` will execute when the request fails
*/
fail: function(fn) {
if (this._erred) {
fn(
this._responseArgs.resp,
this._responseArgs.msg,
this._responseArgs.t
);
} else {
this._errorHandlers.push(fn);
}
return this;
},
catch: function(fn) {
return this.fail(fn);
}
};
function reqwest(o, fn) {
return new Reqwest(o, fn);
}
// normalize newline variants according to spec -> CRLF
function normalize(s) {
return s ? s.replace(/\r?\n/g, "\r\n") : "";
}
function serial(el, cb) {
var n = el.name,
t = el.tagName.toLowerCase(),
optCb = function(o) {
// IE gives value="" even where there is no value attribute
// 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
if (o && !o["disabled"])
cb(
n,
normalize(
o["attributes"]["value"] &&
o["attributes"]["value"]["specified"]
? o["value"]
: o["text"]
)
);
},
ch,
ra,
val,
i;
// don't serialize elements that are disabled or without a name
if (el.disabled || !n) return;
switch (t) {
case "input":
if (!/reset|button|image|file/i.test(el.type)) {
ch = /checkbox/i.test(el.type);
ra = /radio/i.test(el.type);
val = el.value;
// WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
(!(ch || ra) || el.checked) &&
cb(n, normalize(ch && val === "" ? "on" : val));
}
break;
case "textarea":
cb(n, normalize(el.value));
break;
case "select":
if (el.type.toLowerCase() === "select-one") {
optCb(
el.selectedIndex >= 0
? el.options[el.selectedIndex]
: null
);
} else {
for (i = 0; el.length && i < el.length; i++) {
el.options[i].selected && optCb(el.options[i]);
}
}
break;
}
}
// collect up all form elements found from the passed argument elements all
// the way down to child elements; pass a '<form>' or form fields.
// called with 'this'=callback to use for serial() on each element
function eachFormElement() {
var cb = this,
e,
i,
serializeSubtags = function(e, tags) {
var i, j, fa;
for (i = 0; i < tags.length; i++) {
fa = e[byTag](tags[i]);
for (j = 0; j < fa.length; j++) serial(fa[j], cb);
}
};
for (i = 0; i < arguments.length; i++) {
e = arguments[i];
if (/input|select|textarea/i.test(e.tagName)) serial(e, cb);
serializeSubtags(e, ["input", "select", "textarea"]);
}
}
// standard query string style serialization
function serializeQueryString() {
return reqwest.toQueryString(
reqwest.serializeArray.apply(null, arguments)
);
}
// { 'name': 'value', ... } style serialization
function serializeHash() {
var hash = {};
eachFormElement.apply(function(name, value) {
if (name in hash) {
hash[name] &&
!isArray(hash[name]) &&
(hash[name] = [hash[name]]);
hash[name].push(value);
} else hash[name] = value;
}, arguments);
return hash;
}
// [ { name: 'name', value: 'value' }, ... ] style serialization
reqwest.serializeArray = function() {
var arr = [];
eachFormElement.apply(function(name, value) {
arr.push({ name: name, value: value });
}, arguments);
return arr;
};
reqwest.serialize = function() {
if (arguments.length === 0) return "";
var opt,
fn,
args = Array.prototype.slice.call(arguments, 0);
opt = args.pop();
opt && opt.nodeType && args.push(opt) && (opt = null);
opt && (opt = opt.type);
if (opt == "map") fn = serializeHash;
else if (opt == "array") fn = reqwest.serializeArray;
else fn = serializeQueryString;
return fn.apply(null, args);
};
reqwest.toQueryString = function(o, trad) {
var prefix,
i,
traditional = trad || false,
s = [],
enc = encodeURIComponent,
add = function(key, value) {
// If value is a function, invoke it and return its value
value =
"function" === typeof value
? value()
: value == null
? ""
: value;
s[s.length] = enc(key) + "=" + enc(value);
};
// If an array was passed in, assume that it is an array of form elements.
if (isArray(o)) {
for (i = 0; o && i < o.length; i++)
add(o[i]["name"], o[i]["value"]);
} else {
// If traditional, encode the "old" way (the way 1.3.2 or older
// did it), otherwise encode params recursively.
for (prefix in o) {
if (o.hasOwnProperty(prefix))
buildParams(prefix, o[prefix], traditional, add);
}
}
// spaces should be + according to spec
return s.join("&").replace(/%20/g, "+");
};
function buildParams(prefix, obj, traditional, add) {
var name,
i,
v,
rbracket = /\[\]$/;
if (isArray(obj)) {
// Serialize array item.
for (i = 0; obj && i < obj.length; i++) {
v = obj[i];
if (traditional || rbracket.test(prefix)) {
// Treat each array item as a scalar.
add(prefix, v);
} else {
buildParams(
prefix + "[" + (typeof v === "object" ? i : "") + "]",
v,
traditional,
add
);
}
}
} else if (obj && obj.toString() === "[object Object]") {
// Serialize object item.
for (name in obj) {
buildParams(
prefix + "[" + name + "]",
obj[name],
traditional,
add
);
}
} else {
// Serialize scalar item.
add(prefix, obj);
}
}
reqwest.getcallbackPrefix = function() {
return callbackPrefix;
};
// jQuery and Zepto compatibility, differences can be remapped here so you can call
// .ajax.compat(options, callback)
reqwest.compat = function(o, fn) {
if (o) {
o["type"] && (o["method"] = o["type"]) && delete o["type"];
o["dataType"] && (o["type"] = o["dataType"]);
o["jsonpCallback"] &&
(o["jsonpCallbackName"] = o["jsonpCallback"]) &&
delete o["jsonpCallback"];
o["jsonp"] && (o["jsonpCallback"] = o["jsonp"]);
}
return new Reqwest(o, fn);
};
reqwest.ajaxSetup = function(options) {
options = options || {};
for (var k in options) {
globalSetupOptions[k] = options[k];
}
};
return reqwest;
});