Skip to main content

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;
});