Source: phin.js

"use strict";

const realHttp = require("http");
const https = require("https");
const url = require("url");
const qs = require("querystring");
const zlib = require("zlib");
const util = require("util");

/**
* phin options object
* @typedef {Object} phinOptions
* @property {string} url - URL to request (autodetect infers from this URL)
* @property {string} [method=GET] - Request method ('GET', 'POST', etc.)
* @property {string|Object} [data] - Data to send as request body (if Object, data is JSON.stringified unless content-type header is present and set to 'application/x-www-url-form-encoded' in which case the data will be encoded as a query string.)
* @property {Object} [headers={}] - Request headers
* @property {string} [auth=autodetect] - Request authentiction in "user:password" format
* @property {string} [hostname=autodetect] - URL hostname
* @property {Number} [port=autodetect] - URL port
* @property {string} [path=autodetect] - URL path
*/

/**
* Response data callback
* @callback phinResponseCallback
* @param {?(Error|string)} error - Error if any occurred in request, otherwise null.
* @param {?http.serverResponse} phinResponse - phin response object. Like <a href="https://nodejs.org/api/http.html#http_class_http_serverresponse">http.ServerResponse</a> but has a body property containing response body
*/

/**
* Sends an HTTP request
* @param {phinOptions|string} options - phin options object (or string for auto-detection)
* @param {phinResponseCallback} [callback=null] - Callback to which data is sent upon request completion
* @param {Object} [httpModule=require('http')] - HTTP module injection (for testing)
*/
const phin = (opts, cb, injectedHttp) => {
	if (typeof(opts) !== "string") {
		if (!opts.hasOwnProperty("url")) {
			throw new Error("Missing url option from options for request method.");
		}
	}

	var addr;
	if (typeof opts === "object") {
		addr = url.parse(opts.url);
	} else {
		addr = url.parse(opts);
	}
	var options = {
		"hostname": addr.hostname,
		"port": addr.protocol.toLowerCase() === "http:" ? 80 : 443,
		"path": addr.path,
		"method": "GET",
		"headers": {},
		"auth": (addr.auth || null)
	};

	if (typeof opts === "object") {
		options = Object.assign(options, opts);
	}
	options.port = Number(options.port);

	if (options.compressed === true) {
		options.headers["accept-encoding"] = "gzip, deflate";
	}

	var req;
	const resHandler = (res) => {
		var stream = res;
		if (options.compressed === true) {
			if (res.headers["content-encoding"] === "gzip") {
				stream = res.pipe(zlib.createGunzip());
			} else if (res.headers["content-encoding"] === "deflate") {
				stream = res.pipe(zlib.createInflate());
			}
		}
		res.body = new Buffer([]);
		stream.on("data", (chunk) => {
			res.body = Buffer.concat([res.body, chunk]);
		});
		stream.on("end", () => {
			if (cb) {
				cb(null, res);
			}
		});
	};

	// Dependency injection for testing

	const http = injectedHttp || realHttp;

	switch (addr.protocol.toLowerCase()) {
		case "http:":
			req = http.request(options, resHandler);
			break;
		case "https:":
			req = https.request(options, resHandler);
			break;
		default:
			if (cb) {
				cb(new Error("Invalid / unknown URL protocol. Expected HTTP or HTTPS."), null);
			}
			return;
	}

	req.on("error", (err) => {
		if (cb) {
			cb(err, null);
		}
	});

	if (opts.hasOwnProperty("data")) {
		var postData = opts.data;
		if (!(opts.data instanceof Buffer) && typeof opts.data === "object") {
			const contentType = options.headers["Content-Type"] || options.headers["content-type"];
			if (contentType === "application/x-www-url-form-encoded") {
				postData = qs.stringify(opts.data);
			} else {
				try {
					postData = JSON.stringify(opts.data);
				}
				catch (err) {
					cb(new Error("Couldn't stringify object. (Likely due to a circular reference.)"), null);
				}
			}
		}
		req.write(postData);
	}
	req.end();
};

// If we're running Node.js 8+, let's promisify it

if (util.promisify) {
	phin[util.promisify.custom] = (opts, http) => {
		return new Promise((resolve, reject) => {
			phin(opts, (err, res) => {
				if (err) {
					reject(err);
				} else {
					resolve(res);
				}
			}, http);
		});
	};
}

module.exports = phin;