'use strict';
/**
* @module main
*/
/* core modules */
var http = require('http');
var fs = require('fs');
var ReadStream = fs.ReadStream;
var IncomingMessage = http.IncomingMessage;
/* local modules */
var tools = require('./tools.js');
var Request = require('./request.js');
/* 3rd party modules */
exports.FormData = require('form-data');
var mmm = require('mmmagic');
var magic = new mmm.Magic(mmm.MAGIC_MIME_TYPE);
// private section
/**
* Wrapper for all the responses having a body
*
* @private
*/
var requestWrapper = function(options, file, callback) {
if (!callback) {
callback = file;
file = false;
}
var request = exports.createHttpClient(options);
tools.checkType('callback', callback, 'function');
request.send(function(err, res) {
if (err) {
callback(err);
return;
}
if (options.stream === true) {
callback(null, request.stdResult());
return;
}
switch (file) {
case false:
// buffer
request.saveBuffer(function(err, stdResult) {
if (err) {
callback(err);
return;
}
callback(null, stdResult);
});
break;
case null:
// simulate writing to /dev/null, carefully under node.js v0.10
callback(null, request.stdResult());
res.response.resume();
break;
default:
// save to file
tools.checkType('file', file, 'string');
request.saveFile(file, function(err, stdResult) {
if (err) {
callback(err);
return;
}
callback(null, stdResult);
});
break;
}
});
};
// public section
/**
* Constructs a Request object for wrapping your own HTTP method / functionality
*
* @param {module:request~options} options The options Object taken by the {@link module:request~Request} constructor
* @returns {module:request~Request} A new instance of the {@link module:request~Request} class
*/
exports.createHttpClient = function(options) {
return new Request(options);
};
/**
* HTTP GET method wrapper
*
* @param {module:request~options | String} options The options Object taken by the {@link Request} constructor, filtered by {@link module:tools.shortHand}. options.method = 'GET' and it can not be overridden. If a String is passed, it is handled as options = {url: options}
* @param {String|null} file Optional; specify a filesystem path to save the response body as file instead of Buffer. Passing null turns off the data listeners
* @param {module:main~callback} callback Completion callback
*
* @example
// shorthand syntax, buffered response
http.get('http://localhost/get', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.buffer.toString());
});
// save the response to file with a progress callback
http.get({
url: 'http://localhost/get',
progress: function (current, total) {
console.log('downloaded %d bytes from %d', current, total);
}
}, 'get.bin', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.file);
});
// with explicit options, use a HTTP proxy, and limit the number of redirects
http.get({
url: 'http://localhost/get',
proxy: {
host: 'localhost',
port: 3128
},
maxRedirects: 2
}, function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.buffer.toString());
});
*/
exports.get = function(options, file, callback) {
options = tools.shortHand(options, 'get');
requestWrapper(options, file, callback);
};
/**
* HTTP HEAD method wrapper
*
* @param {module:request~options | String} options The options Object taken by the {@link Request} constructor, filtered by {@link module:tools.shortHand}. options.method = 'HEAD' and it can not be overridden. If a String is passed, it is handled as options = {url: options}
* @param {module:main~callback} callback Completion callback
*
* @example
// shorthand syntax, default options
http.head('http://localhost/head', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers);
});
*/
exports.head = function(options, callback) {
options = tools.shortHand(options, 'head');
var request = exports.createHttpClient(options);
tools.checkType('callback', callback, 'function');
request.send(function(err, res) {
if (err) {
callback(err);
return;
}
callback(null, request.stdResult());
res.response.resume();
});
};
/**
* HTTP DELETE method wrapper
*
* @param {module:request~options | String} options The options Object taken by the {@link Request} constructor, filtered by {@link module:tools.shortHand}. options.method = 'DELETE' and it can not be overridden. If a String is passed, it is handled as options = {url: options}
* @param {String|null} file Optional; specify a filesystem path to save the response body as file instead of Buffer. Passing null turns off the data listeners
* @param {module:main~callback} callback Completion callback
*
* @example
// shorthand syntax, buffered response
http.delete('http://localhost/delete', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.buffer.toString());
});
// with explicit options object
http.delete({
url: 'http://localhost/delete'
}, function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.file);
});
*/
exports.delete = function(options, file, callback) {
options = tools.shortHand(options, 'delete');
requestWrapper(options, file, callback);
};
/**
* HTTP POST method wrapper
*
* @param {module:request~options | String} options The options Object taken by the {@link Request} constructor, filtered by {@link module:tools.shortHand}. options.method = 'POST' and it can not be overridden. If a String is passed, it is handled as options = {url: options}
* @param {String|null} file Optional; specify a filesystem path to save the response body as file instead of Buffer. Passing null turns off the data listeners
* @param {module:main~callback} callback Completion callback
*
* @example
// shorthand syntax, buffered response, content-type = application/x-www-form-urlencoded
// param1=value1¶m2=value2 are stripped from the URL and sent via the request body
// do not pass the query values by using URL encoding
// the URL encoding is done automatically
http.post('http://localhost/post?param1=value1¶m2=value2', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.buffer.toString());
});
// explicit POST request, saves the response body to file
// for Buffer or String reqBody, the content-length header is reqBody.length
var reqBody = {
param1: 'value1',
param2: 'value2'
};
// this serialization also does URL encoding so you won't have to
reqBody = querystring.stringify(reqBody);
http.post({
url: 'http://localhost/post',
reqBody: new Buffer(reqBody),
headers: {
// specify how to handle the request, http-request makes no assumptions
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
}
}, 'post.bin', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.file);
});
// multipart/form-data request built with form-data (3rd party module)
// you have to use the http export of FormData
// the instanceof operator fails otherwise
// implementing a manual check is not very elegant
var form = new http.FormData();
form.append('param1', 'value1');
form.append('param2', 'value2');
http.post({
url: 'http://localhost/post',
reqBody: form
}, function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.buffer.toString());
});
*/
exports.post = function(options, file, callback) {
var reqBody;
options = tools.shortHand(options, 'post');
// shorthand POST request
if (!options.reqBody && options.url.indexOf('?') !== -1) {
reqBody = options.url.split('?');
options.url = reqBody[0];
reqBody = tools.urlEncode(reqBody[1]);
options.reqBody = new Buffer(reqBody);
options.headers['content-type'] = 'application/x-www-form-urlencoded;charset=utf-8';
}
if (options.reqBody instanceof exports.FormData) {
options.reqBody.getLength(function(err, length) {
if (err) {
callback(err);
return;
}
options.headers['content-length'] = length;
options.headers = options.reqBody.getHeaders(options.headers);
requestWrapper(options, file, callback);
});
return;
}
requestWrapper(options, file, callback);
};
/**
* HTTP PUT method wrapper
*
* @param {module:request~options | String} options The options Object taken by the {@link Request} constructor, filtered by {@link module:tools.shortHand}. options.method = 'PUT' and it can not be overridden. If a String is passed, it is handled as options = {url: options}
* @param {String|null} file Optional; specify a filesystem path to save the response body as file instead of Buffer. Passing null turns off the data listeners
* @param {module:main~callback} callback Completion callback
*
* @example
// put a buffer
http.put({
url: 'http://localhost/put',
reqBody: new Buffer('data to put'),
headers: {
// if you don't define a type, then mmmagic does it for you
'content-type': 'text/plain'
}
}, function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.buffer.toString());
});
// put a ReadStream created with fs.createReadStream
// http-request knows how to handle a ReadStream
// content-length is taken from the file itself by making a fs.stat call
// content-type is detected automatically by using mmmagic if unspecified
http.put({
url: 'http://localhost/put',
reqBody: fs.createReadStream('/path/to/file')
}, function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers);
});
// pipe a HTTP response (a http.IncomingMessage Object) to a PUT request
// http-request knows how to handle an IncomingMessage
// content-length is taken from the response if it exists
require('http').get('http://example.org/file.ext', function (im) {
http.put({
url: 'http://localhost/put',
reqBody: im,
headers: {
// if you don't define content-type
// it is taken from the response if exists
'content-type': 'application/octet-stream'
}
}, function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers);
});
});
// put a readable stream with known size and type, save the response body to file
http.put({
url: 'http://localhost/put',
reqBody: readableStream,
headers: {
'content-length': 1337,
'content-type': 'text/plain'
}
}, 'put.bin', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res.code, res.headers, res.buffer.toString());
});
*/
exports.put = function(options, file, callback) {
options = tools.shortHand(options, 'put');
if (!options.headers['content-type'] && options.reqBody instanceof Buffer) {
magic.detect(options.reqBody, function(err, mime) {
if (err) {
callback(err);
return;
}
options.headers['content-type'] = mime;
requestWrapper(options, file, callback);
});
return;
}
if (options.reqBody instanceof ReadStream) {
options.reqBody.pause();
fs.stat(options.reqBody.path, function(err, stats) {
if (err) {
callback(err);
return;
}
options.headers['content-length'] = stats.size;
if (!options.headers['content-type']) {
magic.detectFile(options.reqBody.path, function(err, mime) {
if (err) {
callback(err);
return;
}
options.headers['content-type'] = mime;
requestWrapper(options, file, callback);
});
return;
}
requestWrapper(options, file, callback);
});
return;
}
if (options.reqBody instanceof IncomingMessage) {
if (options.reqBody.headers['content-length']) {
options.headers['content-length'] = options.reqBody.headers['content-length'];
}
if (!options.headers['content-type'] && options.reqBody.headers['content-type']) {
options.headers['content-type'] = options.reqBody.headers['content-type'];
}
}
requestWrapper(options, file, callback);
};
/**
* Reads the first chunk from the target URL. Tries to pass back the proper MIME type
*
* @param {String} url The target URL
* @param {Function} callback Completion callback with either {@link module:request~stdError} or the MIME type as string
*
* @example
http.mimeSniff('http://localhost/foo.pdf', function (err, mime) {
if (err) {
console.error(err);
return;
}
console.log(mime); // application/pdf
});
*/
exports.mimeSniff = function(url, callback) {
var request = exports.createHttpClient({
url: url,
method: 'GET',
stream: true,
noCompress: true
});
request.send(function(err, res) {
if (err) {
callback(err);
return;
}
if (!res.stream) {
request.error(new Error('Failed to retrieve the HTTP stream.'), callback);
return;
}
if (res.stream.headers['content-length'] === '0') {
request.error(new Error('The target URL has Content-Length: 0.'), callback);
return;
}
res.stream.resume();
res.stream.on('data', function(chunk) {
request.request.abort();
magic.detect(chunk, function(err, mime) {
if (err) {
request.error(err, callback);
return;
}
callback(null, mime);
});
});
});
};
/**
* Sets the http.Agent.defaultMaxSockets value
*
* @param {Number} value
*
* @example
// use up to 128 concurrent sockets
http.setMaxSockets(128);
*/
exports.setMaxSockets = function(value) {
value = tools.absInt(value);
if (!value) { // fallback to the default
value = 5;
}
http.Agent.defaultMaxSockets = value;
};
// documentation section
/**
* Describes the callback passed to each HTTP method wrapper
*
* @callback module:main~callback
* @param {module:request~stdError} error The standard error or *null* on success
* @param {module:request~stdResult} result The standard result object if error is null
*/