// Generated by CoffeeScript 1.9.3 var Promise, S, _, addErrorData, bhttpAPI, bhttpErrors, concatStream, createCookieJar, debug, debugRequest, debugResponse, devNull, doPayloadRequest, doRedirect, errors, extend, formData, formFixArray, http, https, isStream, makeRequest, ofTypes, packageConfig, prepareCleanup, prepareDefaults, prepareOptions, preparePayload, prepareProtocol, prepareRequest, prepareSession, prepareUrl, processResponse, querystring, redirectGet, redirectUnchanged, sink, spy, stream, streamLength, toughCookie, urlUtil, util; urlUtil = require("url"); querystring = require("querystring"); stream = require("stream"); http = require("http"); https = require("https"); util = require("util"); Promise = require("bluebird"); _ = require("lodash"); S = require("string"); formFixArray = require("form-fix-array"); errors = require("errors"); debug = require("debug"); debugRequest = debug("bhttp:request"); debugResponse = debug("bhttp:response"); extend = require("extend"); devNull = require("dev-null"); formData = require("form-data2"); concatStream = require("concat-stream"); toughCookie = require("tough-cookie"); streamLength = require("stream-length"); sink = require("through2-sink"); spy = require("through2-spy"); packageConfig = require("../package.json"); bhttpErrors = {}; errors.create({ name: "bhttpError", scope: bhttpErrors }); errors.create({ name: "ConflictingOptionsError", parents: bhttpErrors.bhttpError, scope: bhttpErrors }); errors.create({ name: "UnsupportedProtocolError", parents: bhttpErrors.bhttpError, scope: bhttpErrors }); errors.create({ name: "RedirectError", parents: bhttpErrors.bhttpError, scope: bhttpErrors }); errors.create({ name: "MultipartError", parents: bhttpErrors.bhttpError, scope: bhttpErrors }); errors.create({ name: "ConnectionTimeoutError", parents: bhttpErrors.bhttpError, scope: bhttpErrors }); errors.create({ name: "ResponseTimeoutError", parents: bhttpErrors.bhttpError, scope: bhttpErrors }); ofTypes = function(obj, types) { var i, len, match, type; match = false; for (i = 0, len = types.length; i < len; i++) { type = types[i]; match = match || obj instanceof type; } return match; }; addErrorData = function(err, request, response, requestState) { err.request = request; err.response = response; err.requestState = requestState; return err; }; isStream = function(obj) { return (obj != null) && (ofTypes(obj, [stream.Readable, stream.Duplex, stream.Transform]) || obj.hasOwnProperty("_bhttpStreamWrapper")); }; prepareSession = function(request, response, requestState) { debugRequest("preparing session"); return Promise["try"](function() { if (requestState.sessionOptions != null) { request.options = _.merge(_.clone(requestState.sessionOptions), request.options); } if (request.options.headers != null) { request.options.headers = _.clone(request.options.headers, true); } else { request.options.headers = {}; } if (request.options.cookieJar != null) { return Promise["try"](function() { request.cookieJar = request.options.cookieJar; delete request.options.cookieJar; return request.cookieJar.get(request.url); }).then(function(cookieString) { debugRequest("sending cookie string: %s", cookieString); request.options.headers["cookie"] = cookieString; return Promise.resolve([request, response, requestState]); }); } else { return Promise.resolve([request, response, requestState]); } }); }; prepareDefaults = function(request, response, requestState) { debugRequest("preparing defaults"); return Promise["try"](function() { var base, base1, base2, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7; request.responseOptions = { discardResponse: (ref = request.options.discardResponse) != null ? ref : false, keepRedirectResponses: (ref1 = request.options.keepRedirectResponses) != null ? ref1 : false, followRedirects: (ref2 = request.options.followRedirects) != null ? ref2 : true, noDecode: (ref3 = request.options.noDecode) != null ? ref3 : false, decodeJSON: (ref4 = request.options.decodeJSON) != null ? ref4 : false, stream: (ref5 = request.options.stream) != null ? ref5 : false, justPrepare: (ref6 = request.options.justPrepare) != null ? ref6 : false, redirectLimit: (ref7 = request.options.redirectLimit) != null ? ref7 : 10, onDownloadProgress: request.options.onDownloadProgress, responseTimeout: request.options.responseTimeout }; if ((base = request.options).allowChunkedMultipart == null) { base.allowChunkedMultipart = false; } if ((base1 = request.options).forceMultipart == null) { base1.forceMultipart = false; } if ((base2 = request.options.headers)["user-agent"] == null) { base2["user-agent"] = "bhttp/" + packageConfig.version; } request.options.method = request.options.method.toLowerCase(); return Promise.resolve([request, response, requestState]); }); }; prepareUrl = function(request, response, requestState) { debugRequest("preparing URL"); return Promise["try"](function() { var ref, urlOptions; urlOptions = urlUtil.parse(request.url, true); _.extend(request.options, { hostname: urlOptions.hostname, port: urlOptions.port }); request.options.path = urlUtil.format({ pathname: urlOptions.pathname, query: (ref = request.options.query) != null ? ref : urlOptions.query }); request.protocol = S(urlOptions.protocol).chompRight(":").toString(); return Promise.resolve([request, response, requestState]); }); }; prepareProtocol = function(request, response, requestState) { debugRequest("preparing protocol"); return Promise["try"](function() { var base; request.protocolModule = (function() { switch (request.protocol) { case "http": return http; case "https": return https; default: return null; } })(); if (request.protocolModule == null) { return Promise.reject()(new bhttpErrors.UnsupportedProtocolError("The protocol specified (" + protocol + ") is not currently supported by this module.")); } if ((base = request.options).port == null) { base.port = (function() { switch (request.protocol) { case "http": return 80; case "https": return 443; } })(); } return Promise.resolve([request, response, requestState]); }); }; prepareOptions = function(request, response, requestState) { debugRequest("preparing options"); return Promise["try"](function() { var base; if (((request.options.formFields != null) || (request.options.files != null)) && ((request.options.inputStream != null) || (request.options.inputBuffer != null))) { return Promise.reject(addErrorData(new bhttpErrors.ConflictingOptionsError("You cannot define both formFields/files and a raw inputStream or inputBuffer."), request, response, requestState)); } if (request.options.encodeJSON && ((request.options.inputStream != null) || (request.options.inputBuffer != null))) { return Promise.reject(addErrorData(new bhttpErrors.ConflictingOptionsError("You cannot use both encodeJSON and a raw inputStream or inputBuffer.", void 0, "If you meant to JSON-encode the stream, you will currently have to do so manually."), request, response, requestState)); } if (request.responseOptions.stream) { if ((base = request.options).agent == null) { base.agent = false; } } return Promise.resolve([request, response, requestState]); }); }; preparePayload = function(request, response, requestState) { debugRequest("preparing payload"); return Promise["try"](function() { var containsStreams, fieldName, fieldValue, formDataObject, i, len, multipart, ref, ref1, ref2, streamOptions, valueElement; request.onUploadProgress = request.options.onUploadProgress; multipart = request.options.forceMultipart || (request.options.files != null); multipart = multipart || _.any(request.options.formFields, function(item) { return item instanceof Buffer || isStream(item); }); _.extend(request.options.formFields, request.options.files); containsStreams = _.any(request.options.formFields, function(item) { return isStream(item); }); if (request.options.encodeJSON && containsStreams) { return Promise.reject()(new bhttpErrors.ConflictingOptionsError("Sending a JSON-encoded payload containing data from a stream is not currently supported.", void 0, "Either don't use encodeJSON, or read your stream into a string or Buffer.")); } if ((ref = request.options.method) !== "get" && ref !== "head" && ref !== "delete") { if ((request.options.encodeJSON || (request.options.formFields != null)) && !multipart) { debugRequest("got url-encodable form-data"); if (request.options.encodeJSON) { debugRequest("... but encodeJSON was set, so we will send JSON instead"); request.options.headers["content-type"] = "application/json"; request.payload = JSON.stringify((ref1 = request.options.formFields) != null ? ref1 : null); } else if (!_.isEmpty(request.options.formFields)) { request.options.headers["content-type"] = "application/x-www-form-urlencoded"; request.payload = querystring.stringify(formFixArray(request.options.formFields)); } else { request.payload = ""; } request.options.headers["content-length"] = request.payload.length; return Promise.resolve(); } else if ((request.options.formFields != null) && multipart) { debugRequest("got multipart form-data"); formDataObject = new formData(); ref2 = formFixArray(request.options.formFields); for (fieldName in ref2) { fieldValue = ref2[fieldName]; if (!_.isArray(fieldValue)) { fieldValue = [fieldValue]; } for (i = 0, len = fieldValue.length; i < len; i++) { valueElement = fieldValue[i]; if (valueElement._bhttpStreamWrapper != null) { streamOptions = valueElement.options; valueElement = valueElement.stream; } else { streamOptions = {}; } formDataObject.append(fieldName, valueElement, streamOptions); } } request.payloadStream = formDataObject; return Promise["try"](function() { return formDataObject.getHeaders(); }).then(function(headers) { if (headers["content-transfer-encoding"] === "chunked" && !request.options.allowChunkedMultipart) { return Promise.reject(addErrorData(new MultipartError("Most servers do not support chunked transfer encoding for multipart/form-data payloads, and we could not determine the length of all the input streams. See the documentation for more information."), request, response, requestState)); } else { _.extend(request.options.headers, headers); return Promise.resolve(); } }); } else if (request.options.inputStream != null) { debugRequest("got inputStream"); return Promise["try"](function() { var ref3; request.payloadStream = request.options.inputStream; if ((request.payloadStream._bhttpStreamWrapper != null) && ((request.payloadStream.options.contentLength != null) || (request.payloadStream.options.knownLength != null))) { return Promise.resolve((ref3 = request.payloadStream.options.contentLength) != null ? ref3 : request.payloadStream.options.knownLength); } else { return streamLength(request.options.inputStream); } }).then(function(length) { debugRequest("length for inputStream is %s", length); return request.options.headers["content-length"] = length; })["catch"](function(err) { debugRequest("unable to determine inputStream length, switching to chunked transfer encoding"); return request.options.headers["content-transfer-encoding"] = "chunked"; }); } else if (request.options.inputBuffer != null) { debugRequest("got inputBuffer"); if (typeof request.options.inputBuffer === "string") { request.payload = new Buffer(request.options.inputBuffer); } else { request.payload = request.options.inputBuffer; } debugRequest("length for inputBuffer is %s", request.payload.length); request.options.headers["content-length"] = request.payload.length; return Promise.resolve(); } else { return Promise.resolve(); } } else { return Promise.resolve(); } }).then(function() { return Promise.resolve([request, response, requestState]); }); }; prepareCleanup = function(request, response, requestState) { debugRequest("preparing cleanup"); return Promise["try"](function() { var fixedHeaders, i, key, len, ref, ref1, value; ref = ["query", "formFields", "files", "encodeJSON", "inputStream", "inputBuffer", "discardResponse", "keepRedirectResponses", "followRedirects", "noDecode", "decodeJSON", "allowChunkedMultipart", "forceMultipart", "onUploadProgress", "onDownloadProgress"]; for (i = 0, len = ref.length; i < len; i++) { key = ref[i]; delete request.options[key]; } fixedHeaders = {}; ref1 = request.options.headers; for (key in ref1) { value = ref1[key]; fixedHeaders[key.toLowerCase()] = value; } request.options.headers = fixedHeaders; return Promise.resolve([request, response, requestState]); }); }; prepareRequest = function(request, response, requestState) { debugRequest("preparing request"); return Promise["try"](function() { var middlewareFunctions, promiseChain; middlewareFunctions = [prepareSession, prepareDefaults, prepareUrl, prepareProtocol, prepareOptions, preparePayload, prepareCleanup]; promiseChain = Promise.resolve([request, response, requestState]); middlewareFunctions.forEach(function(middleware) { return promiseChain = promiseChain.spread(function(_request, _response, _requestState) { return middleware(_request, _response, _requestState); }); }); return promiseChain; }); }; makeRequest = function(request, response, requestState) { debugRequest("making %s request to %s", request.options.method.toUpperCase(), request.url); return Promise["try"](function() { var req, timeoutTimer; req = request.protocolModule.request(request.options); timeoutTimer = null; return new Promise(function(resolve, reject) { var completedBytes, progressStream, totalBytes; if (request.responseOptions.responseTimeout != null) { debugRequest("setting response timeout timer to " + request.responseOptions.responseTimeout + "ms..."); req.on("socket", function(socket) { var timeoutHandler; timeoutHandler = function() { debugRequest("a response timeout occurred!"); req.abort(); return reject(addErrorData(new bhttpErrors.ResponseTimeoutError("The response timed out."))); }; return timeoutTimer = setTimeout(timeoutHandler, request.responseOptions.responseTimeout); }); } totalBytes = request.options.headers["content-length"]; completedBytes = 0; progressStream = spy(function(chunk) { completedBytes += chunk.length; return req.emit("progress", completedBytes, totalBytes); }); if (request.onUploadProgress != null) { req.on("progress", function(completedBytes, totalBytes) { return request.onUploadProgress(completedBytes, totalBytes, req); }); } if (request.payload != null) { debugRequest("sending payload"); req.emit("progress", request.payload.length, request.payload.length); req.write(request.payload); req.end(); } else if (request.payloadStream != null) { debugRequest("piping payloadStream"); if (request.payloadStream._bhttpStreamWrapper != null) { request.payloadStream.stream.pipe(progressStream).pipe(req); } else { request.payloadStream.pipe(progressStream).pipe(req); } } else { debugRequest("closing request without payload"); req.end(); } req.on("error", function(err) { if (err.code === "ETIMEDOUT") { debugRequest("a connection timeout occurred!"); return reject(addErrorData(new bhttpErrors.ConnectionTimeoutError("The connection timed out."))); } else { return reject(err); } }); return req.on("response", function(res) { if (timeoutTimer != null) { debugResponse("got response in time, clearing response timeout timer"); clearTimeout(timeoutTimer); } return resolve(res); }); }); }).then(function(response) { return Promise.resolve([request, response, requestState]); }); }; processResponse = function(request, response, requestState) { debugResponse("processing response, got status code %s", response.statusCode); return Promise["try"](function() { var cookieHeader, promises; if ((request.cookieJar != null) && (response.headers["set-cookie"] != null)) { promises = (function() { var i, len, ref, results; ref = response.headers["set-cookie"]; results = []; for (i = 0, len = ref.length; i < len; i++) { cookieHeader = ref[i]; debugResponse("storing cookie: %s", cookieHeader); results.push(request.cookieJar.set(cookieHeader, request.url)); } return results; })(); return Promise.all(promises); } else { return Promise.resolve(); } }).then(function() { var completedBytes, progressStream, ref, ref1, totalBytes; response.request = request; response.requestState = requestState; response.redirectHistory = requestState.redirectHistory; if (((ref = response.statusCode) === 301 || ref === 302 || ref === 303 || ref === 307) && request.responseOptions.followRedirects) { if (requestState.redirectHistory.length >= (request.responseOptions.redirectLimit - 1)) { return Promise.reject(addErrorData(new bhttpErrors.RedirectError("The maximum amount of redirects ({request.responseOptions.redirectLimit}) was reached."))); } switch (response.statusCode) { case 301: switch (request.options.method) { case "get": case "head": return redirectUnchanged(request, response, requestState); case "post": case "put": case "patch": case "delete": return Promise.reject(addErrorData(new bhttpErrors.RedirectError("Encountered a 301 redirect for POST, PUT, PATCH or DELETE. RFC says we can't automatically continue."), request, response, requestState)); default: return Promise.reject(addErrorData(new bhttpErrors.RedirectError("Encountered a 301 redirect, but not sure how to proceed for the " + (request.options.method.toUpperCase()) + " method."))); } break; case 302: case 303: return redirectGet(request, response, requestState); case 307: if (request.containsStreams && ((ref1 = request.options.method) !== "get" && ref1 !== "head")) { return Promise.reject(addErrorData(new bhttpErrors.RedirectError("Encountered a 307 redirect for POST, PUT or DELETE, but your payload contained (single-use) streams. We therefore can't automatically follow the redirect."), request, response, requestState)); } else { return redirectUnchanged(request, response, requestState); } } } else if (request.responseOptions.discardResponse) { response.pipe(devNull()); return Promise.resolve(response); } else { totalBytes = response.headers["content-length"]; if (totalBytes != null) { totalBytes = parseInt(totalBytes); } completedBytes = 0; progressStream = sink(function(chunk) { completedBytes += chunk.length; return response.emit("progress", completedBytes, totalBytes); }); if (request.responseOptions.onDownloadProgress != null) { response.on("progress", function(completedBytes, totalBytes) { return request.responseOptions.onDownloadProgress(completedBytes, totalBytes, response); }); } return new Promise(function(resolve, reject) { var _on, _progressStreamAttached, _resume, attachProgressStream; _resume = response.resume.bind(response); _on = response.on.bind(response); _progressStreamAttached = false; attachProgressStream = function() { if (!_progressStreamAttached) { debugResponse("attaching progress stream"); _progressStreamAttached = true; return response.pipe(progressStream); } }; response.on = function(eventName, handler) { debugResponse("'on' called, " + eventName); if (eventName === "data" || eventName === "readable") { attachProgressStream(); } return _on(eventName, handler); }; response.resume = function() { attachProgressStream(); return _resume(); }; if (request.responseOptions.stream) { return resolve(response); } else { response.on("error", function(err) { return reject(err); }); return response.pipe(concatStream(function(body) { var err, ref2; if (request.responseOptions.decodeJSON || (((ref2 = response.headers["content-type"]) != null ? ref2 : "").split(";")[0] === "application/json" && !request.responseOptions.noDecode)) { try { response.body = JSON.parse(body); } catch (_error) { err = _error; reject(err); } } else { response.body = body; } return resolve(response); })); } }); } }).then(function(response) { return Promise.resolve([request, response, requestState]); }); }; doPayloadRequest = function(url, data, options, callback) { if (isStream(data)) { options.inputStream = data; } else if (ofTypes(data, [Buffer]) || typeof data === "string") { options.inputBuffer = data; } else { options.formFields = data; } return this.request(url, options, callback); }; redirectGet = function(request, response, requestState) { debugResponse("following forced-GET redirect to %s", response.headers["location"]); return Promise["try"](function() { var i, key, len, options, ref; options = _.clone(requestState.originalOptions); options.method = "get"; ref = ["inputBuffer", "inputStream", "files", "formFields"]; for (i = 0, len = ref.length; i < len; i++) { key = ref[i]; delete options[key]; } return doRedirect(request, response, requestState, options); }); }; redirectUnchanged = function(request, response, requestState) { debugResponse("following same-method redirect to %s", response.headers["location"]); return Promise["try"](function() { var options; options = _.clone(requestState.originalOptions); return doRedirect(request, response, requestState, options); }); }; doRedirect = function(request, response, requestState, newOptions) { return Promise["try"](function() { if (!request.responseOptions.keepRedirectResponses) { response.pipe(devNull()); } requestState.redirectHistory.push(response); return bhttpAPI._doRequest(urlUtil.resolve(request.url, response.headers["location"]), newOptions, requestState); }); }; createCookieJar = function(jar) { return { set: function(cookie, url) { return new Promise((function(_this) { return function(resolve, reject) { return _this.jar.setCookie(cookie, url, function(err, cookie) { if (err) { return reject(err); } else { return resolve(cookie); } }); }; })(this)); }, get: function(url) { return new Promise((function(_this) { return function(resolve, reject) { return _this.jar.getCookieString(url, function(err, cookies) { if (err) { return reject(err); } else { return resolve(cookies); } }); }; })(this)); }, jar: jar }; }; bhttpAPI = { head: function(url, options, callback) { if (options == null) { options = {}; } options.method = "head"; return this.request(url, options, callback); }, get: function(url, options, callback) { if (options == null) { options = {}; } options.method = "get"; return this.request(url, options, callback); }, post: function(url, data, options, callback) { if (options == null) { options = {}; } options.method = "post"; return doPayloadRequest.bind(this)(url, data, options, callback); }, put: function(url, data, options, callback) { if (options == null) { options = {}; } options.method = "put"; return doPayloadRequest.bind(this)(url, data, options, callback); }, patch: function(url, data, options, callback) { if (options == null) { options = {}; } options.method = "patch"; return doPayloadRequest.bind(this)(url, data, options, callback); }, "delete": function(url, options, callback) { if (options == null) { options = {}; } options.method = "delete"; return this.request(url, options, callback); }, request: function(url, options, callback) { if (options == null) { options = {}; } return this._doRequest(url, options).nodeify(callback); }, _doRequest: function(url, options, requestState) { return Promise["try"]((function(_this) { return function() { var ref, request, response; request = { url: url, options: _.clone(options) }; response = null; if (requestState == null) { requestState = { originalOptions: _.clone(options), redirectHistory: [] }; } if (requestState.sessionOptions == null) { requestState.sessionOptions = (ref = _this._sessionOptions) != null ? ref : {}; } return prepareRequest(request, response, requestState); }; })(this)).spread((function(_this) { return function(request, response, requestState) { if (request.responseOptions.justPrepare) { return Promise.resolve([request, response, requestState]); } else { return Promise["try"](function() { return bhttpAPI.executeRequest(request, response, requestState); }).spread(function(request, response, requestState) { return Promise.resolve(response); }); } }; })(this)); }, executeRequest: function(request, response, requestState) { return Promise["try"](function() { return makeRequest(request, response, requestState); }).spread(function(request, response, requestState) { return processResponse(request, response, requestState); }); }, session: function(options) { var key, session, value; if (options == null) { options = {}; } options = _.clone(options); session = {}; for (key in this) { value = this[key]; if (value instanceof Function) { value = value.bind(session); } session[key] = value; } if (options.cookieJar == null) { options.cookieJar = createCookieJar(new toughCookie.CookieJar()); } else if (options.cookieJar === false) { delete options.cookieJar; } else { options.cookieJar = createCookieJar(options.cookieJar); } session._sessionOptions = options; return session; }, wrapStream: function(stream, options) { return { _bhttpStreamWrapper: true, stream: stream, options: options }; } }; extend(bhttpAPI, bhttpErrors); module.exports = bhttpAPI;