Comparing version 0.4.0 to 0.5.0
{ | ||
"name": "popsicle", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"description": "Simple HTTP requests for node and the browser", | ||
@@ -12,10 +12,18 @@ "main": "popsicle.js", | ||
"browser": { | ||
"request": false, | ||
"form-data": false, | ||
"buffer": false, | ||
"./package.json": false | ||
"concat-stream": false, | ||
"http": false, | ||
"https": false, | ||
"url": false, | ||
"zlib": false, | ||
"infinity-agent": false, | ||
"statuses": false, | ||
"through2": false, | ||
"tough-cookie": false | ||
}, | ||
"scripts": { | ||
"lint": "standard", | ||
"test": "npm run lint && gulp test" | ||
"filesize": "du -hs popsicle.js", | ||
"test": "npm run lint && gulp test && npm run filesize" | ||
}, | ||
@@ -47,3 +55,3 @@ "repository": { | ||
"body-parser": "^1.9.2", | ||
"chai": "^1.9.1", | ||
"chai": "^2.1.1", | ||
"es6-promise": "^2.0.0", | ||
@@ -68,5 +76,9 @@ "express": "^4.10.2", | ||
"dependencies": { | ||
"concat-stream": "^1.4.7", | ||
"form-data": "^0.2.0", | ||
"request": "^2.48.0" | ||
"infinity-agent": "^1.0.2", | ||
"statuses": "^1.2.1", | ||
"through2": "^0.6.3", | ||
"tough-cookie": "^0.12.1" | ||
} | ||
} |
773
popsicle.js
@@ -26,3 +26,4 @@ /* global define */ | ||
var abortRequest | ||
var jar | ||
var isHostObject | ||
var createRequest | ||
@@ -32,4 +33,4 @@ var parseRawHeaders | ||
if (typeof Promise === 'undefined') { | ||
var PROMISE_ERROR_MESSAGE = (isNode ? 'global' : 'window') + '.Promise ' + | ||
'is undefined and should be polyfilled. Check out ' + | ||
var PROMISE_ERROR_MESSAGE = (!isNode ? 'global' : 'window') + | ||
'.Promise is undefined and must be polyfilled. Check out ' + | ||
'https://github.com/jakearchibald/es6-promise for more information.' | ||
@@ -131,12 +132,12 @@ | ||
* | ||
* @param {Popsicle} self | ||
* @param {Request} req | ||
* @return {Error} | ||
*/ | ||
function abortError (self) { | ||
if (self._error) { | ||
return self._error | ||
function abortError (req) { | ||
if (req._error) { | ||
return req._error | ||
} | ||
if (!self.timedout) { | ||
var abortedError = self.error('Request aborted') | ||
if (!req.timedout) { | ||
var abortedError = req.error('Request aborted') | ||
abortedError.abort = true | ||
@@ -146,4 +147,4 @@ return abortedError | ||
var timeout = self.timeout | ||
var timedoutError = self.error('Timeout of ' + timeout + 'ms exceeded') | ||
var timeout = req.timeout | ||
var timedoutError = req.error('Timeout of ' + timeout + 'ms exceeded') | ||
timedoutError.timeout = timeout | ||
@@ -156,8 +157,8 @@ return timedoutError | ||
* | ||
* @param {Popsicle} self | ||
* @param {Error} e | ||
* @param {Request} req | ||
* @param {Error} e | ||
* @return {Error} | ||
*/ | ||
function parseError (self, e) { | ||
var err = self.error('Unable to parse the response body') | ||
function parseError (req, e) { | ||
var err = req.error('Unable to parse the response body') | ||
err.parse = true | ||
@@ -171,8 +172,8 @@ err.original = e | ||
* | ||
* @param {Popsicle} self | ||
* @param {Error} e | ||
* @param {Request} req | ||
* @param {Error} e | ||
* @return {Error} | ||
*/ | ||
function cspError (self, e) { | ||
var err = self.error('Refused to connect to "' + self.fullUrl() + '"') | ||
function cspError (req, e) { | ||
var err = req.error('Refused to connect to "' + req.fullUrl() + '"') | ||
err.csp = true | ||
@@ -186,7 +187,7 @@ err.original = e | ||
* | ||
* @param {Popsicle} self | ||
* @param {Request} req | ||
* @return {Error} | ||
*/ | ||
function unavailableError (self) { | ||
var err = self.error('Unable to connect to "' + self.fullUrl() + '"') | ||
function unavailableError (req) { | ||
var err = req.error('Unable to connect to "' + req.fullUrl() + '"') | ||
err.unavailable = true | ||
@@ -199,7 +200,7 @@ return err | ||
* | ||
* @param {Popsicle} self | ||
* @param {Request} req | ||
* @return {Error} | ||
*/ | ||
function blockedError (self) { | ||
var err = self.error('The request to "' + self.fullUrl() + '" was blocked') | ||
function blockedError (req) { | ||
var err = req.error('The request to "' + req.fullUrl() + '" was blocked') | ||
err.blocked = true | ||
@@ -210,2 +211,14 @@ return err | ||
/** | ||
* Create a maximum redirection error. | ||
* | ||
* @param {Request} req | ||
* @return {Error} | ||
*/ | ||
function redirectError (req) { | ||
var err = req.error('Maximum number of redirects exceeded') | ||
err.maxRedirects = req.maxRedirects | ||
return err | ||
} | ||
/** | ||
* Return the content type from a header string. | ||
@@ -231,9 +244,5 @@ * | ||
try { | ||
return encodeURIComponent(str) | ||
.replace(/[!'()]/g, root.escape) | ||
.replace(/\*/g, '%2A') | ||
} catch (e) { | ||
return '' | ||
} | ||
return encodeURIComponent(str) | ||
.replace(/[!'()]/g, root.escape) | ||
.replace(/\*/g, '%2A') | ||
} | ||
@@ -328,30 +337,2 @@ | ||
/** | ||
* Check whether the object is already natively supported. | ||
* | ||
* @param {*} object | ||
* @return {Boolean} | ||
*/ | ||
var isHostObject | ||
if (isNode) { | ||
isHostObject = function (object) { | ||
return object instanceof Buffer || object instanceof FormData | ||
} | ||
} else { | ||
isHostObject = function (object) { | ||
var str = Object.prototype.toString.call(object) | ||
switch (str) { | ||
case '[object File]': | ||
case '[object Blob]': | ||
case '[object FormData]': | ||
case '[object ArrayBuffer]': | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
} | ||
/** | ||
* Convert an object into a form data instance. | ||
@@ -362,3 +343,3 @@ * | ||
*/ | ||
function toFormData (obj) { | ||
function form (obj) { | ||
var form = new FormData() | ||
@@ -378,10 +359,10 @@ | ||
* | ||
* @param {Request} request | ||
* @param {Request} req | ||
*/ | ||
function stringifyRequest (request) { | ||
var body = request.body | ||
function stringifyRequest (req) { | ||
var body = req.body | ||
// Convert primitives types into strings. | ||
if (!isObject(body)) { | ||
request.body = body == null ? null : String(body) | ||
req.body = body == null ? null : String(body) | ||
@@ -396,3 +377,3 @@ return | ||
var type = request.type() | ||
var type = req.type() | ||
@@ -403,11 +384,11 @@ // Set the default mime type to be JSON if none exists. | ||
request.type(type) | ||
req.type(type) | ||
} | ||
if (JSON_MIME_REGEXP.test(type)) { | ||
request.body = JSON.stringify(body) | ||
req.body = JSON.stringify(body) | ||
} else if (FORM_MIME_REGEXP.test(type)) { | ||
request.body = toFormData(body) | ||
req.body = form(body) | ||
} else if (QUERY_MIME_REGEXP.test(type)) { | ||
request.body = stringifyQuery(body) | ||
req.body = stringifyQuery(body) | ||
} | ||
@@ -419,13 +400,13 @@ } | ||
* | ||
* @param {Response} response | ||
* @param {Response} res | ||
* @return {Promise} | ||
*/ | ||
function parseResponse (response) { | ||
var body = response.body | ||
var type = response.type() | ||
function parseResponse (res) { | ||
var body = res.body | ||
var type = res.type() | ||
if (body === '') { | ||
response.body = null | ||
res.body = null | ||
return response | ||
return res | ||
} | ||
@@ -435,8 +416,8 @@ | ||
if (JSON_MIME_REGEXP.test(type)) { | ||
response.body = body === '' ? null : JSON.parse(body) | ||
res.body = body === '' ? null : JSON.parse(body) | ||
} else if (QUERY_MIME_REGEXP.test(type)) { | ||
response.body = parseQuery(body) | ||
res.body = parseQuery(body) | ||
} | ||
} catch (e) { | ||
return Promise.reject(parseError(response, e)) | ||
return Promise.reject(parseError(res, e)) | ||
} | ||
@@ -448,24 +429,31 @@ } | ||
* | ||
* @param {Request} request | ||
* @param {Request} req | ||
*/ | ||
function defaultAccept (request) { | ||
function defaultHeaders (req) { | ||
// If we have no accept header set already, default to accepting | ||
// everything. This is needed because otherwise Firefox defaults to | ||
// an accept header of `html/xml`. | ||
if (!request.get('Accept')) { | ||
request.set('Accept', '*/*') | ||
if (!req.get('Accept')) { | ||
req.set('Accept', '*/*') | ||
} | ||
} | ||
/** | ||
* Correct the content type request header. | ||
* | ||
* @param {Request} request | ||
*/ | ||
function correctType (request) { | ||
// Specify a default user agent in node. | ||
if (!req.get('User-Agent')) { | ||
req.set('User-Agent', 'https://github.com/blakeembrey/popsicle') | ||
} | ||
// Accept zipped responses. | ||
if (!req.get('Accept-Encoding')) { | ||
req.set('Accept-Encoding', 'gzip,deflate') | ||
} | ||
// Remove the `Content-Type` header from form data requests. The node | ||
// `request` module supports `form-data` to automatically add headers, | ||
// and the browser will ses it on `xhr.send` (when it's not already set). | ||
if (request.body instanceof FormData) { | ||
request.remove('Content-Type') | ||
// and the browser will set it on `xhr.send` (only when it doesn't exist). | ||
if (req.body instanceof FormData) { | ||
if (isNode) { | ||
req.set(req.body.getHeaders()) | ||
} else { | ||
req.remove('Content-Type') | ||
} | ||
} | ||
@@ -477,9 +465,14 @@ } | ||
* | ||
* @param {Request} request | ||
* @param {Request} req | ||
*/ | ||
function removeListeners (request) { | ||
delete request._before | ||
delete request._after | ||
delete request._always | ||
delete request._progress | ||
function removeListeners (req) { | ||
req._before = undefined | ||
req._after = undefined | ||
req._always = undefined | ||
req._progress = undefined | ||
req._errored = undefined | ||
req._raw = undefined | ||
req.body = undefined | ||
} | ||
@@ -490,8 +483,8 @@ | ||
* | ||
* @param {Request} request | ||
* @param {Request} req | ||
* @return {Promise} | ||
*/ | ||
function checkAborted (request) { | ||
if (request.aborted) { | ||
return Promise.reject(abortError(request)) | ||
function checkAborted (req) { | ||
if (req.aborted) { | ||
return Promise.reject(abortError(req)) | ||
} | ||
@@ -503,9 +496,9 @@ } | ||
* | ||
* @param {Request} self | ||
* @param {Request} req | ||
* @param {Object} headers | ||
*/ | ||
function setHeaders (self, headers) { | ||
function setHeaders (req, headers) { | ||
if (headers) { | ||
Object.keys(headers).forEach(function (key) { | ||
self.set(key, headers[key]) | ||
req.set(key, headers[key]) | ||
}) | ||
@@ -516,2 +509,18 @@ } | ||
/** | ||
* Get all headers case-sensitive. | ||
* | ||
* @param {Request} req | ||
* @return {Object} | ||
*/ | ||
function getHeaders (req) { | ||
var headers = {} | ||
Object.keys(req.headers).forEach(function (key) { | ||
headers[req.name(key)] = req.headers[key] | ||
}) | ||
return headers | ||
} | ||
/** | ||
* Lower-case the header name. Allow usage of `Referrer` and `Referer`. | ||
@@ -651,3 +660,3 @@ * | ||
function chain (fns, arg) { | ||
return fns.reduce(function (promise, fn) { | ||
return (fns || []).reduce(function (promise, fn) { | ||
return promise.then(function () { | ||
@@ -662,11 +671,11 @@ return fn(arg) | ||
* | ||
* @param {Request} self | ||
* @param {Request} req | ||
*/ | ||
function setup (self) { | ||
var timeout = self.timeout | ||
function setup (req) { | ||
var timeout = req.timeout | ||
if (timeout) { | ||
self._timer = setTimeout(function () { | ||
self.timedout = true | ||
self.abort() | ||
req._timer = setTimeout(function () { | ||
req.timedout = true | ||
req.abort() | ||
}, timeout) | ||
@@ -676,13 +685,10 @@ } | ||
// Set the request to "opened", disables any new listeners. | ||
self.opened = true | ||
req.opened = true | ||
return chain(self._before, self) | ||
return chain(req._before, req) | ||
.then(function () { | ||
return createRequest(self) | ||
return createRequest(req) | ||
}) | ||
.then(function (response) { | ||
response.request = self | ||
self.response = response | ||
return chain(self._after, response) | ||
.then(function (res) { | ||
return chain(req._after, res) | ||
}) | ||
@@ -694,9 +700,9 @@ .catch(function (err) { | ||
return chain(self._always, self).then(reject) | ||
return chain(req._always, req).then(reject) | ||
}) | ||
.then(function () { | ||
return chain(self._always, self) | ||
return chain(req._always, req) | ||
}) | ||
.then(function () { | ||
return self.response | ||
return req.response | ||
}) | ||
@@ -708,12 +714,12 @@ } | ||
* | ||
* @param {Request} self | ||
* @param {Request} req | ||
* @return {Promise} | ||
*/ | ||
function create (self) { | ||
function create (req) { | ||
// Setup a new promise request if none exists. | ||
if (!self._promise) { | ||
self._promise = setup(self) | ||
if (!req._promise) { | ||
req._promise = setup(req) | ||
} | ||
return self._promise | ||
return req._promise | ||
} | ||
@@ -732,5 +738,5 @@ | ||
* | ||
* @param {String} key | ||
* @param {String} value | ||
* @return {Header} | ||
* @param {String} key | ||
* @param {String} value | ||
* @return {Headers} | ||
*/ | ||
@@ -753,2 +759,20 @@ Headers.prototype.set = function (key, value) { | ||
/** | ||
* Append a header value. | ||
* | ||
* @param {String} key | ||
* @param {String} value | ||
* @return {Headers} | ||
*/ | ||
Headers.prototype.append = function (key, value) { | ||
var prev = this.get(key) | ||
var val = value | ||
if (prev) { | ||
val = Array.isArray(prev) ? prev.concat(value) : [prev].concat(value) | ||
} | ||
return this.set(key, val) | ||
} | ||
/** | ||
* Get the original case-sensitive header name. | ||
@@ -770,2 +794,6 @@ * | ||
Headers.prototype.get = function (header) { | ||
if (header == null) { | ||
return getHeaders(this) | ||
} | ||
return this.headers[lowerHeader(header)] | ||
@@ -806,6 +834,9 @@ } | ||
* | ||
* @param {Request} request | ||
* @param {Request} req | ||
*/ | ||
function Response (request) { | ||
function Response (req) { | ||
Headers.call(this) | ||
this.request = req | ||
req.response = this | ||
} | ||
@@ -829,38 +860,2 @@ | ||
/** | ||
* Check whether the response was an info response. Status >= 100 < 200. | ||
* | ||
* @return {Boolean} | ||
*/ | ||
Response.prototype.info = function () { | ||
return this.statusType() === 1 | ||
} | ||
/** | ||
* Check whether the response was ok. Status >= 200 < 300. | ||
* | ||
* @return {Boolean} | ||
*/ | ||
Response.prototype.ok = function () { | ||
return this.statusType() === 2 | ||
} | ||
/** | ||
* Check whether the response was a client error. Status >= 400 < 500. | ||
* | ||
* @return {Boolean} | ||
*/ | ||
Response.prototype.clientError = function () { | ||
return this.statusType() === 4 | ||
} | ||
/** | ||
* Check whether the response was a server error. Status >= 500 < 600. | ||
* | ||
* @return {Boolean} | ||
*/ | ||
Response.prototype.serverError = function () { | ||
return this.statusType() === 5 | ||
} | ||
/** | ||
* Create a popsicle error instance. | ||
@@ -884,2 +879,4 @@ * | ||
var query = options.query | ||
var stream = options.stream === true | ||
var parse = options.parse !== false | ||
@@ -895,5 +892,14 @@ // Request options. | ||
this.jar = options.jar | ||
this.withCredentials = options.withCredentials === true | ||
this.maxRedirects = num(options.maxRedirects) | ||
this.rejectUnauthorized = options.rejectUnauthorized !== false | ||
this.agent = options.agent | ||
// Default redirect count. | ||
if (isNaN(this.maxRedirects) || this.maxRedirects < 0) { | ||
this.maxRedirects = 10 | ||
} | ||
// Browser specific options. | ||
this.withCredentials = options.withCredentials === true | ||
// Progress state. | ||
@@ -905,3 +911,3 @@ this.uploaded = this.downloaded = this.completed = 0 | ||
// Set request headers. | ||
setHeaders(this, options.headers) | ||
this.set(options.headers) | ||
@@ -923,8 +929,27 @@ // Request state. | ||
this.before(checkAborted) | ||
this.before(defaultAccept) | ||
this.before(stringifyRequest) | ||
this.before(correctType) | ||
this.before(defaultHeaders) | ||
this.after(parseResponse) | ||
if (isNode) { | ||
this.before(contentLength) | ||
if (this.jar) { | ||
this.before(getCookieJar) | ||
this.after(setCookieJar) | ||
} | ||
} | ||
// Support streaming responses under node. | ||
if (!stream) { | ||
if (isNode) { | ||
this.after(streamResponse) | ||
} | ||
if (parse) { | ||
this.after(parseResponse) | ||
} | ||
} else if (!isNode) { | ||
throw new Error('Streaming is only available in node') | ||
} | ||
this.always(removeListeners) | ||
@@ -988,8 +1013,8 @@ } | ||
this.aborted = true | ||
// Set everything to completed. | ||
this.downloaded = this.uploaded = this.completed = 1 | ||
// Abort and emit the final progress event. | ||
abortRequest(this) | ||
if (this._raw) { | ||
this._raw.abort() | ||
} | ||
emitProgress(this, this._progress) | ||
@@ -1049,91 +1074,116 @@ clearTimeout(this._timer) | ||
if (isNode) { | ||
var request = require('request') | ||
var version = require('./package.json').version | ||
var http = require('http') | ||
var https = require('https') | ||
var urlLib = require('url') | ||
var zlib = require('zlib') | ||
var agent = require('infinity-agent') | ||
var statuses = require('statuses') | ||
var through2 = require('through2') | ||
var tough = require('tough-cookie') | ||
/** | ||
* Return options sanitized for the request module. | ||
* Stream node response. | ||
* | ||
* @param {Request} self | ||
* @return {Object} | ||
* @param {Response} res | ||
* @return {Promise} | ||
*/ | ||
var requestOptions = function (self) { | ||
var request = {} | ||
var streamResponse = function (res) { | ||
var concat = require('concat-stream') | ||
request.url = self.fullUrl() | ||
request.method = self.method | ||
request.jar = self.jar | ||
request.headers = { | ||
'User-Agent': 'node-popsicle/' + version | ||
} | ||
return new Promise(function (resolve, reject) { | ||
var concatStream = concat({ | ||
encoding: 'string' | ||
}, function (data) { | ||
// Update the response `body`. | ||
res.body = data | ||
// Add headers with the correct case names. | ||
Object.keys(self.headers).forEach(function (header) { | ||
request.headers[self.name(header)] = self.get(header) | ||
return resolve() | ||
}) | ||
res.body.once('error', reject) | ||
res.body.pipe(concatStream) | ||
}) | ||
} | ||
// The `request` module supports form data under a private property. | ||
if (self.body instanceof FormData) { | ||
request._form = self.body | ||
} else { | ||
request.body = self.body | ||
} | ||
/** | ||
* Set the default content length. | ||
* | ||
* @param {Request} req | ||
*/ | ||
var contentLength = function (req) { | ||
var length = 0 | ||
var body = req.body | ||
if (self.rejectUnauthorized) { | ||
request.rejectUnauthorized = true | ||
if (body && !req.get('Content-Length')) { | ||
if (!Buffer.isBuffer(body)) { | ||
if (Array.isArray(body)) { | ||
for (var i = 0; i < body.length; i++) { | ||
length += body[i].length | ||
} | ||
} else { | ||
body = new Buffer(body) | ||
length = body.length | ||
} | ||
} else { | ||
length = body.length | ||
} | ||
if (length) { | ||
req.set('Content-Length', length) | ||
} | ||
} | ||
return request | ||
} | ||
/** | ||
* Return the byte length of an input. | ||
* Read cookies from the cookie jar. | ||
* | ||
* @param {(String|Buffer)} data | ||
* @return {Number} | ||
* @param {Request} req | ||
* @return {Promise} | ||
*/ | ||
var byteLength = function (data) { | ||
if (Buffer.isBuffer(data)) { | ||
return data.length | ||
} | ||
var getCookieJar = function (req) { | ||
return new Promise(function (resolve, reject) { | ||
req.jar.getCookies(req.url, function (err, cookies) { | ||
if (err) { | ||
return reject(err) | ||
} | ||
if (typeof data === 'string') { | ||
return Buffer.byteLength(data) | ||
} | ||
if (cookies.length) { | ||
req.append('Cookie', cookies.join('; ')) | ||
} | ||
return 0 | ||
return resolve() | ||
}) | ||
}) | ||
} | ||
/** | ||
* Track the current download size. | ||
* Put cookies in the cookie jar. | ||
* | ||
* @param {Request} self | ||
* @param {request} request | ||
* @param {Response} res | ||
* @return {Promise} | ||
*/ | ||
var trackRequestProgress = function (self, request) { | ||
function onRequest (request) { | ||
var write = request.write | ||
var setCookieJar = function (res) { | ||
return new Promise(function (resolve, reject) { | ||
var cookies = res.get('Set-Cookie') | ||
self.uploadTotal = num(request.getHeader('Content-Length')) | ||
if (!cookies) { | ||
return resolve() | ||
} | ||
// Override `Request.prototype.write` to track amount of sent data. | ||
request.write = function (data) { | ||
setUploadSize(self, self.uploadSize + byteLength(data)) | ||
return write.apply(this, arguments) | ||
if (!Array.isArray(cookies)) { | ||
cookies = [cookies] | ||
} | ||
} | ||
function onResponse (response) { | ||
response.on('data', onResponseData) | ||
self.downloadTotal = num(response.headers['content-length']) | ||
setUploadFinished(self) | ||
} | ||
var setCookies = cookies.map(function (cookie) { | ||
return new Promise(function (resolve, reject) { | ||
var req = res.request | ||
function onResponseData (data) { | ||
// Data should always be a `Buffer` instance. | ||
setDownloadSize(self, self.downloadSize + data.length) | ||
} | ||
req.jar.setCookie(cookie, req.url, function (err) { | ||
return err ? reject(err) : resolve() | ||
}) | ||
}) | ||
}) | ||
request.on('request', onRequest) | ||
request.on('response', onResponse) | ||
return resolve(Promise.all(setCookies)) | ||
}) | ||
} | ||
@@ -1168,38 +1218,111 @@ | ||
* | ||
* @param {Request} self | ||
* @param {Request} req | ||
* @return {Promise} | ||
*/ | ||
createRequest = function (self) { | ||
createRequest = function (req) { | ||
return new Promise(function (resolve, reject) { | ||
var opts = requestOptions(self) | ||
var body = req.body | ||
var redirectCount = 0 | ||
var headers = req.get() | ||
var req = request(opts, function (err, response) { | ||
// Clean up listeners. | ||
delete self._request | ||
setDownloadFinished(self) | ||
/** | ||
* Track upload progress through a stream. | ||
*/ | ||
var requestProxy = through2(function (chunk, enc, callback) { | ||
setUploadSize(req, req.uploadSize + chunk.length) | ||
callback(null, chunk) | ||
}, function (callback) { | ||
setUploadFinished(req) | ||
callback(req.aborted ? abortError(req) : null) | ||
}) | ||
if (err) { | ||
// Node.js core error (ECONNRESET, EPIPE). | ||
if (typeof err.code === 'string') { | ||
return reject(unavailableError(self)) | ||
/** | ||
* Track download progress through a stream. | ||
*/ | ||
var responseProxy = through2(function (chunk, enc, callback) { | ||
setDownloadSize(req, req.downloadSize + chunk.length) | ||
callback(null, chunk) | ||
}, function (callback) { | ||
setDownloadFinished(req) | ||
callback(req.aborted ? abortError(req) : null) | ||
}) | ||
/** | ||
* Create the HTTP request. | ||
* | ||
* @param {String} url | ||
*/ | ||
function get (url) { | ||
var arg = urlLib.parse(url) | ||
var fn = arg.protocol === 'https:' ? https : http | ||
arg.headers = headers | ||
arg.method = req.method | ||
arg.rejectUnauthorized = req.rejectUnauthorized | ||
arg.agent = req.agent || agent(arg) | ||
var request = fn.request(arg) | ||
request.once('response', function (response) { | ||
var statusCode = response.statusCode | ||
var stream = response | ||
// Handle HTTP redirections. | ||
if (statuses.redirect[statusCode] && response.headers.location) { | ||
// Discard response. | ||
response.resume() | ||
if (++redirectCount > req.maxRedirects) { | ||
reject(redirectError(req)) | ||
return | ||
} | ||
get(urlLib.resolve(url, response.headers.location)) | ||
return | ||
} | ||
return reject(err) | ||
} | ||
req.downloadTotal = num(response.headers['content-length']) | ||
var res = new Response() | ||
// Track download progress. | ||
stream.pipe(responseProxy) | ||
stream = responseProxy | ||
res.body = response.body | ||
res.status = response.statusCode | ||
res.set(parseRawHeaders(response)) | ||
// Decode zipped responses. | ||
if (['gzip', 'deflate'].indexOf(response.headers['content-encoding']) !== -1) { | ||
var unzip = zlib.createUnzip() | ||
stream.pipe(unzip) | ||
stream = unzip | ||
} | ||
return resolve(res) | ||
}) | ||
var res = new Response(req) | ||
req.on('abort', function () { | ||
return reject(abortError(self)) | ||
}) | ||
res.body = stream | ||
res.status = response.statusCode | ||
res.set(parseRawHeaders(response)) | ||
self._request = req | ||
trackRequestProgress(self, req) | ||
return resolve(res) | ||
}) | ||
request.once('error', function (err) { | ||
return reject(req.aborted ? abortError(req) : unavailableError(req)) | ||
}) | ||
req._raw = request | ||
req.uploadTotal = num(request.getHeader('Content-Length')) | ||
requestProxy.pipe(request) | ||
// Pipe the body to the stream. | ||
if (body) { | ||
if (typeof body.pipe === 'function') { | ||
body.pipe(requestProxy) | ||
} else { | ||
requestProxy.end(body) | ||
} | ||
} else { | ||
requestProxy.end() | ||
} | ||
} | ||
get(req.fullUrl()) | ||
}) | ||
@@ -1209,11 +1332,19 @@ } | ||
/** | ||
* Abort a running node request. | ||
* Check for host objects in node. | ||
* | ||
* @param {Request} self | ||
* @param {*} object | ||
* @return {Boolean} | ||
*/ | ||
abortRequest = function (self) { | ||
if (self._request) { | ||
self._request.abort() | ||
} | ||
isHostObject = function (object) { | ||
return object instanceof Buffer || object instanceof FormData | ||
} | ||
/** | ||
* Create a cookie jar in node. | ||
* | ||
* @return {Object} | ||
*/ | ||
jar = function () { | ||
return new tough.CookieJar() | ||
} | ||
} else { | ||
@@ -1265,17 +1396,17 @@ /** | ||
* | ||
* @param {Request} self | ||
* @param {Request} req | ||
* @return {Promise} | ||
*/ | ||
createRequest = function (self) { | ||
createRequest = function (req) { | ||
return new Promise(function (resolve, reject) { | ||
var url = self.fullUrl() | ||
var method = self.method | ||
var res = new Response() | ||
var url = req.fullUrl() | ||
var method = req.method | ||
var res = new Response(req) | ||
// Loading HTTP resources from HTTPS is restricted and uncatchable. | ||
if (window.location.protocol === 'https:' && /^http\:/.test(url)) { | ||
return reject(blockedError(self)) | ||
return reject(blockedError(req)) | ||
} | ||
var xhr = self._xhr = getXHR() | ||
var xhr = req._raw = getXHR() | ||
@@ -1289,3 +1420,3 @@ xhr.onreadystatechange = function () { | ||
// Try setting the total download size. | ||
self.downloadTotal = num(res.get('Content-Length')) | ||
req.downloadTotal = num(res.get('Content-Length')) | ||
@@ -1295,18 +1426,16 @@ // Trigger upload finished after we get the response length. | ||
// `xhr` object invalid. | ||
setUploadFinished(self) | ||
setUploadFinished(req) | ||
} | ||
if (xhr.readyState === 4) { | ||
// Clean up listeners. | ||
delete self._xhr | ||
setDownloadFinished(self) | ||
setDownloadFinished(req) | ||
// Handle the aborted state internally, PhantomJS doesn't reset | ||
// `xhr.status` to zero on abort. | ||
if (self.aborted) { | ||
return reject(abortError(self)) | ||
if (req.aborted) { | ||
return reject(abortError(req)) | ||
} | ||
if (xhr.status === 0) { | ||
return reject(unavailableError(self)) | ||
return reject(unavailableError(req)) | ||
} | ||
@@ -1323,6 +1452,6 @@ | ||
if (e.lengthComputable) { | ||
self.downloadTotal = e.total | ||
req.downloadTotal = e.total | ||
} | ||
setDownloadSize(self, e.loaded) | ||
setDownloadSize(req, e.loaded) | ||
} | ||
@@ -1332,13 +1461,11 @@ | ||
if (method === 'GET' || method === 'HEAD' || !xhr.upload) { | ||
xhr.upload = {} | ||
self.uploadTotal = 0 | ||
setUploadSize(self, 0) | ||
req.uploadTotal = 0 | ||
setUploadSize(req, 0) | ||
} else { | ||
xhr.upload.onprogress = function (e) { | ||
if (e.lengthComputable) { | ||
self.uploadTotal = e.total | ||
req.uploadTotal = e.total | ||
} | ||
setUploadSize(self, e.loaded) | ||
setUploadSize(req, e.loaded) | ||
} | ||
@@ -1351,7 +1478,7 @@ } | ||
} catch (e) { | ||
return reject(cspError(self, e)) | ||
return reject(cspError(req, e)) | ||
} | ||
// Send cookies with CORS. | ||
if (self.withCredentials) { | ||
if (req.withCredentials) { | ||
xhr.withCredentials = true | ||
@@ -1361,7 +1488,7 @@ } | ||
// Set all headers with original casing. | ||
Object.keys(self.headers).forEach(function (header) { | ||
xhr.setRequestHeader(self.name(header), self.get(header)) | ||
Object.keys(req.headers).forEach(function (header) { | ||
xhr.setRequestHeader(req.name(header), req.get(header)) | ||
}) | ||
xhr.send(self.body) | ||
xhr.send(req.body) | ||
}) | ||
@@ -1371,11 +1498,29 @@ } | ||
/** | ||
* Abort a running XMLHttpRequest. | ||
* Check for host objects in the browser. | ||
* | ||
* @param {Request} self | ||
* @param {*} object | ||
* @return {Boolean} | ||
*/ | ||
abortRequest = function (self) { | ||
if (self._xhr) { | ||
self._xhr.abort() | ||
isHostObject = function (object) { | ||
var str = Object.prototype.toString.call(object) | ||
switch (str) { | ||
case '[object File]': | ||
case '[object Blob]': | ||
case '[object FormData]': | ||
case '[object ArrayBuffer]': | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
/** | ||
* Throw an error in browsers where `jar` is not supported. | ||
* | ||
* @throws {Error} | ||
*/ | ||
jar = function () { | ||
throw new Error('Cookie jars are not supported on the browser') | ||
} | ||
} | ||
@@ -1406,24 +1551,8 @@ | ||
/** | ||
* Initialize a form data instance. | ||
* Expose utilities. | ||
*/ | ||
popsicle.form = function (params) { | ||
return toFormData(params) | ||
} | ||
popsicle.jar = jar | ||
popsicle.form = form | ||
/** | ||
* Support cookie jars (on Node). | ||
* | ||
* @return {Object} | ||
*/ | ||
if (isNode) { | ||
popsicle.jar = function () { | ||
return request.jar() | ||
} | ||
} else { | ||
popsicle.jar = function () { | ||
throw new Error('Cookie jars are not supported on the browser') | ||
} | ||
} | ||
/** | ||
* Expose the `Request` and `Response` constructors. | ||
@@ -1430,0 +1559,0 @@ */ |
105
README.md
@@ -10,6 +10,6 @@ # ![Popsicle](https://cdn.rawgit.com/blakeembrey/popsicle/master/logo.svg) | ||
```javascript | ||
request('/users.json') | ||
popsicle('/users.json') | ||
.then(function (res) { | ||
console.log(res.body); //=> { ... } | ||
}); | ||
console.log(res.body) //=> { ... } | ||
}) | ||
``` | ||
@@ -35,6 +35,6 @@ | ||
// Node and browserify | ||
require('es6-promise').polyfill(); | ||
require('es6-promise').polyfill() | ||
// Browsers | ||
window.ES6Promise.polyfill(); | ||
window.ES6Promise.polyfill() | ||
``` | ||
@@ -45,6 +45,6 @@ | ||
```javascript | ||
var request = require('popsicle'); | ||
// var request = window.popsicle; | ||
var popsicle = require('popsicle') | ||
// var popsicle = window.popsicle | ||
request({ | ||
popsicle({ | ||
method: 'POST', | ||
@@ -61,6 +61,6 @@ url: 'http://example.com/api/users', | ||
.then(function (res) { | ||
console.log(res.status); // => 200 | ||
console.log(res.body); //=> { ... } | ||
console.log(res.get('Content-Type')); //=> 'application/json' | ||
}); | ||
console.log(res.status) // => 200 | ||
console.log(res.body) //=> { ... } | ||
console.log(res.get('Content-Type')) //=> 'application/json' | ||
}) | ||
``` | ||
@@ -76,2 +76,3 @@ | ||
* **timeout** The number of milliseconds before cancelling the request (default: `Infinity`) | ||
* **parse** Optionally skip response parsing (default: `true`) | ||
@@ -81,3 +82,6 @@ **Node only** | ||
* **jar** An instance of a cookie jar (default: `null`) | ||
* **agent** Custom HTTP pooling agent (default: [infinity-agent](https://github.com/floatdrop/infinity-agent)) | ||
* **maxRedirects** Override the number of redirects to allow (default: `10`) | ||
* **rejectUnauthorized** Reject invalid SSL certificates (default: `true`) | ||
* **stream** Stream the HTTP response body (default: `false`) | ||
@@ -93,12 +97,11 @@ **Browser only** | ||
```javascript | ||
request({ | ||
popsicle({ | ||
url: 'http://example.com/api/users', | ||
body: { | ||
username: 'blakeembrey', | ||
profileImage: fs.createReadStream('image.png') | ||
username: 'blakeembrey' | ||
}, | ||
headers: { | ||
'Content-Type': 'multipart/form-data' | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}); | ||
}) | ||
``` | ||
@@ -111,12 +114,12 @@ | ||
```javascript | ||
var form = request.form({ | ||
name: 'Blake Embrey', | ||
image: '...' | ||
}); | ||
var form = popsicle.form({ | ||
username: 'blakeembrey', | ||
profileImage: fs.createReadStream('image.png') | ||
}) | ||
request({ | ||
popsicle({ | ||
method: 'POST', | ||
url: '/users', | ||
body: form | ||
}); | ||
}) | ||
``` | ||
@@ -126,14 +129,14 @@ | ||
All requests can be aborted during execution by calling `Request#abort`. Requests won't normally start until chained anyway, but this will also abort the request before it starts. | ||
All requests can be aborted before or during execution by calling `Request#abort`. | ||
```javascript | ||
var req = request('http://example.com'); | ||
var req = popsicle('http://example.com') | ||
setTimeout(function () { | ||
req.abort(); | ||
}, 100); | ||
req.abort() | ||
}, 100) | ||
req.catch(function (err) { | ||
console.log(err); //=> { message: 'Request aborted', aborted: true } | ||
}); | ||
console.log(err) //=> { message: 'Request aborted', aborted: true } | ||
}) | ||
``` | ||
@@ -156,14 +159,14 @@ | ||
```javascript | ||
var req = request('http://example.com'); | ||
var req = popsicle('http://example.com') | ||
req.uploaded; //=> 0 | ||
req.downloaded; //=> 0 | ||
req.uploaded //=> 0 | ||
req.downloaded //=> 0 | ||
req.progress(function (e) { | ||
console.log(e); //=> { uploaded: 1, downloaded: 0, completed: 0.5, aborted: false } | ||
}); | ||
console.log(e) //=> { uploaded: 1, downloaded: 0, completed: 0.5, aborted: false } | ||
}) | ||
req.then(function (res) { | ||
console.log(req.downloaded); //=> 1 | ||
}); | ||
console.log(req.downloaded) //=> 1 | ||
}) | ||
``` | ||
@@ -176,9 +179,9 @@ | ||
```javascript | ||
var jar = request.jar(); | ||
var jar = request.jar() | ||
request({ | ||
popsicle({ | ||
method: 'POST', | ||
url: '/users', | ||
jar: jar | ||
}); | ||
}) | ||
``` | ||
@@ -195,3 +198,3 @@ | ||
```javascript | ||
request('/users') | ||
popsicle('/users') | ||
.then(function (res) { | ||
@@ -202,3 +205,3 @@ // Things worked! | ||
// Something broke. | ||
}); | ||
}) | ||
``` | ||
@@ -211,3 +214,3 @@ | ||
```javascript | ||
request('/users') | ||
popsicle('/users') | ||
.exec(function (err, res) { | ||
@@ -219,3 +222,3 @@ if (err) { | ||
// Success! | ||
}); | ||
}) | ||
``` | ||
@@ -231,6 +234,2 @@ | ||
* **statusType()** Return an integer with the HTTP status type (E.g. `200 -> 2`) | ||
* **info()** Return a boolean indicating a HTTP status code between 100 and 199 | ||
* **ok()** Return a boolean indicating a HTTP status code between 200 and 299 | ||
* **clientError()** Return a boolean indicating a HTTP status code between 400 and 499 | ||
* **serverError()** Return a boolean indicating a HTTP status code between 500 and 599 | ||
* **get(key)** Retrieve a HTTP header using a case-insensitive key | ||
@@ -250,2 +249,3 @@ * **name(key)** Retrieve the original HTTP header name using a case-insensitive key | ||
* **csp error** Request violates the documents Content Security Policy (browsers, `err.csp`) | ||
* **max redirects error** Number of HTTP redirects exceeded (node, `err.maxRedirects`) | ||
@@ -264,2 +264,3 @@ ### Plugins | ||
* [Constants](https://github.com/blakeembrey/popsicle-constants) - Replace constants in the URL string | ||
* [Limit](https://github.com/blakeembrey/popsicle-limit) - Transparently handle API rate limits | ||
@@ -273,11 +274,11 @@ #### Creating Plugins | ||
return function (req) { | ||
req.url = url + req.url; | ||
}; | ||
req.url = url + req.url | ||
} | ||
} | ||
request('/user') | ||
popsicle('/user') | ||
.use(prefix('http://example.com')) | ||
.then(function (res) { | ||
console.log(res.request.url); //=> "http://example.com/user" | ||
}); | ||
console.log(res.request.url) //=> "http://example.com/user" | ||
}) | ||
``` | ||
@@ -284,0 +285,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
50636
1281
297
6
2
+ Addedconcat-stream@^1.4.7
+ Addedinfinity-agent@^1.0.2
+ Addedstatuses@^1.2.1
+ Addedthrough2@^0.6.3
+ Addedtough-cookie@^0.12.1
+ Addedbuffer-from@1.1.2(transitive)
+ Addedconcat-stream@1.6.2(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addedinfinity-agent@1.0.2(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@0.0.11.0.0(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedreadable-stream@1.0.342.3.8(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedstatuses@1.5.0(transitive)
+ Addedstring_decoder@0.10.311.1.1(transitive)
+ Addedthrough2@0.6.5(transitive)
+ Addedtough-cookie@0.12.1(transitive)
+ Addedtypedarray@0.0.6(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedxtend@4.0.2(transitive)
- Removedrequest@^2.48.0
- Removedajv@6.12.6(transitive)
- Removedasn1@0.2.6(transitive)
- Removedassert-plus@1.0.0(transitive)
- Removedasynckit@0.4.0(transitive)
- Removedaws-sign2@0.7.0(transitive)
- Removedaws4@1.13.2(transitive)
- Removedbcrypt-pbkdf@1.0.2(transitive)
- Removedcaseless@0.12.0(transitive)
- Removedcombined-stream@1.0.8(transitive)
- Removedcore-util-is@1.0.2(transitive)
- Removeddashdash@1.14.1(transitive)
- Removeddelayed-stream@1.0.0(transitive)
- Removedecc-jsbn@0.1.2(transitive)
- Removedextend@3.0.2(transitive)
- Removedextsprintf@1.3.0(transitive)
- Removedfast-deep-equal@3.1.3(transitive)
- Removedfast-json-stable-stringify@2.1.0(transitive)
- Removedforever-agent@0.6.1(transitive)
- Removedform-data@2.3.3(transitive)
- Removedgetpass@0.1.7(transitive)
- Removedhar-schema@2.0.0(transitive)
- Removedhar-validator@5.1.5(transitive)
- Removedhttp-signature@1.2.0(transitive)
- Removedis-typedarray@1.0.0(transitive)
- Removedisstream@0.1.2(transitive)
- Removedjsbn@0.1.1(transitive)
- Removedjson-schema@0.4.0(transitive)
- Removedjson-schema-traverse@0.4.1(transitive)
- Removedjson-stringify-safe@5.0.1(transitive)
- Removedjsprim@1.4.2(transitive)
- Removedmime-db@1.52.0(transitive)
- Removedmime-types@2.1.35(transitive)
- Removedoauth-sign@0.9.0(transitive)
- Removedperformance-now@2.1.0(transitive)
- Removedpsl@1.9.0(transitive)
- Removedqs@6.5.3(transitive)
- Removedrequest@2.88.2(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedsshpk@1.18.0(transitive)
- Removedtough-cookie@2.5.0(transitive)
- Removedtunnel-agent@0.6.0(transitive)
- Removedtweetnacl@0.14.5(transitive)
- Removeduri-js@4.4.1(transitive)
- Removeduuid@3.4.0(transitive)
- Removedverror@1.10.0(transitive)