Comparing version 2.1.6 to 3.0.0
287
lib/http.js
@@ -45,3 +45,3 @@ 'use strict'; | ||
const debug = require('debug')('http-call'); | ||
const debug = require('debug')('http'); | ||
@@ -66,2 +66,34 @@ function concat(stream) { | ||
function caseInsensitiveObject() { | ||
let lowercaseKey = k => typeof k === 'string' ? k.toLowerCase() : k; | ||
return new Proxy({}, { | ||
get: (t, k) => { | ||
k = lowercaseKey(k); | ||
return t[k]; | ||
}, | ||
set: (t, k, v) => { | ||
k = lowercaseKey(k); | ||
t[k] = v; | ||
return true; | ||
}, | ||
deleteProperty: (t, k) => { | ||
k = lowercaseKey(k); | ||
if (k in t) return false; | ||
return delete t[k]; | ||
}, | ||
has: function (t, k) { | ||
k = lowercaseKey(k); | ||
return k in t; | ||
} | ||
}); | ||
} | ||
function lowercaseHeaders(headers) { | ||
let newHeaders = caseInsensitiveObject(); | ||
for (let [k, v] of Object.entries(headers)) { | ||
newHeaders[k] = v; | ||
} | ||
return newHeaders; | ||
} | ||
/** | ||
@@ -83,6 +115,4 @@ * Utility for simple HTTP calls | ||
*/ | ||
static async get(url, options = {}) { | ||
options.method = 'GET'; | ||
let http = await this.request(url, options); | ||
return this._getNextBody(http); | ||
static get(url, options = {}) { | ||
return this.request(url, { ...options, method: 'GET' }); | ||
} | ||
@@ -101,6 +131,4 @@ | ||
*/ | ||
static async post(url, options = {}) { | ||
options.method = 'POST'; | ||
let http = await this.request(url, options); | ||
return http.body; | ||
static post(url, options = {}) { | ||
return this.request(url, { ...options, method: 'POST' }); | ||
} | ||
@@ -119,6 +147,4 @@ | ||
*/ | ||
static async put(url, options = {}) { | ||
options.method = 'PUT'; | ||
let http = await this.request(url, options); | ||
return http.body; | ||
static put(url, options = {}) { | ||
return this.request(url, { ...options, method: 'PUT' }); | ||
} | ||
@@ -138,5 +164,3 @@ | ||
static async patch(url, options = {}) { | ||
options.method = 'PATCH'; | ||
let http = await this.request(url, options); | ||
return http.body; | ||
return this.request(url, { ...options, method: 'PATCH' }); | ||
} | ||
@@ -156,5 +180,3 @@ | ||
static async delete(url, options = {}) { | ||
options.method = 'DELETE'; | ||
let http = await this.request(url, options); | ||
return http.body; | ||
return this.request(url, { ...options, method: 'DELETE' }); | ||
} | ||
@@ -174,7 +196,4 @@ | ||
*/ | ||
static async stream(url, options = {}) { | ||
options.method = options.method || 'GET'; | ||
options.raw = true; | ||
let http = await this.request(url, options); | ||
return http.response; | ||
static stream(url, options = {}) { | ||
return this.request(url, { ...options, raw: true }); | ||
} | ||
@@ -188,46 +207,102 @@ | ||
static defaults(options = {}) { | ||
return class CustomHTTP extends HTTP { | ||
get defaultOptions() { | ||
return { | ||
...super.defaultOptions, | ||
...options | ||
}; | ||
} | ||
}; | ||
} | ||
// instance properties | ||
get method() { | ||
return this.options.method; | ||
} | ||
get statusCode() { | ||
if (!this.response) return 0; | ||
return this.response.statusCode; | ||
} | ||
get secure() { | ||
return this.options.protocol === 'https:'; | ||
} | ||
get url() { | ||
return `${this.options.protocol}//${this.options.host}${this.options.path}`; | ||
} | ||
set url(input) { | ||
let u = _url2.default.parse(input); | ||
this.options.protocol = u.protocol || this.options.protocol; | ||
this.options.host = u.hostname || this.defaultOptions.host || 'localhost'; | ||
this.options.path = u.path || '/'; | ||
this.options.agent = this.options.agent || _proxy2.default.agent(this.secure); | ||
this.options.port = parseInt(u.port || this.defaultOptions.port || (this.secure ? 443 : 80)); | ||
} | ||
get headers() { | ||
if (!this.response) return {}; | ||
return this.response.headers; | ||
} | ||
get partial() { | ||
if (this.method !== 'GET' || this.options.partial) return true; | ||
return !(this.headers['next-range'] && this.body instanceof Array); | ||
} | ||
get defaultOptions() { | ||
return { | ||
method: 'GET', | ||
host: 'localhost', | ||
protocol: 'https:', | ||
path: '/', | ||
raw: false, | ||
partial: false, | ||
headers: { | ||
'user-agent': `${_package2.default.name}/${_package2.default.version} node-${process.version}` | ||
} | ||
}; | ||
} | ||
constructor(url, options = {}) { | ||
this.method = 'GET'; | ||
this.host = 'localhost'; | ||
this.port = 0; | ||
this.protocol = 'https:'; | ||
this.path = '/'; | ||
this.raw = false; | ||
this.partial = false; | ||
this.headers = { | ||
'user-agent': `${_package2.default.name}/${_package2.default.version} node-${process.version}` | ||
this.options = { | ||
...this.defaultOptions, | ||
...options, | ||
headers: lowercaseHeaders({ | ||
...this.defaultOptions.headers, | ||
...options.headers | ||
}) | ||
}; | ||
if (!url) throw new Error('no url provided'); | ||
this.options = options; | ||
let headers = Object.assign(this.headers, options.headers); | ||
Object.assign(this, options); | ||
this.headers = headers; | ||
let u = _url2.default.parse(url); | ||
this.protocol = u.protocol || this.protocol; | ||
this.host = u.hostname || this.host; | ||
this.port = u.port || this.port || (this.protocol === 'https:' ? 443 : 80); | ||
this.path = u.path || this.path; | ||
if (options.body) this.parseBody(options.body); | ||
this.body = undefined; | ||
this.agent = _proxy2.default.agent(this.protocol === 'https:'); | ||
if (this.agent) debug('proxy: %j', this.agent.options); | ||
this.url = url; | ||
if (this.options.body) this._parseBody(this.options.body); | ||
} | ||
async _request(retries = 0) { | ||
async _request() { | ||
this._debugRequest(); | ||
try { | ||
debug(`--> ${this.method} ${this.url}`); | ||
this.response = await this.performRequest(); | ||
debug(`<-- ${this.method} ${this.url} ${this.response.statusCode}`); | ||
this.response = await this._performRequest(); | ||
} catch (err) { | ||
return this.maybeRetry(err, retries); | ||
return this._maybeRetry(err); | ||
} | ||
if (this.response.statusCode >= 200 && this.response.statusCode < 300) { | ||
if (!this.raw) this.body = await this.parse(this.response); | ||
} else throw new HTTPError(this, (await this.parse(this.response))); | ||
if (this._shouldParseResponseBody) await this._parse(); | ||
this._debugResponse(); | ||
if (this._responseRedirect) return this._redirect(); | ||
if (!this._responseOK) { | ||
throw new HTTPError(this); | ||
} | ||
if (!this.partial) await this._getNextRange(); | ||
} | ||
async maybeRetry(err, retries) { | ||
async _redirect() { | ||
if (!this._redirectRetries) this._redirectRetries = 0; | ||
this._redirectRetries++; | ||
if (this._redirectRetries > 10) throw new Error(`Redirect loop at ${this.url}`); | ||
if (!this.headers.location) throw new Error('Redirect with no location header'); | ||
this.url = this.headers.location; | ||
await this._request(); | ||
} | ||
async _maybeRetry(err) { | ||
if (!this._errorRetries) this._errorRetries = 0; | ||
this._errorRetries++; | ||
const allowed = err => { | ||
if (retries >= 5) return false; | ||
if (this._errorRetries > 5) return false; | ||
if (!err.code) return false; | ||
@@ -239,4 +314,4 @@ if (err.code === 'ENOTFOUND') return true; | ||
let noise = Math.random() * 100; | ||
await this._wait((1 << retries) * 1000 + noise); | ||
await this._request(retries + 1); | ||
await this._wait((1 << this._errorRetries) * 1000 + noise); | ||
await this._request(); | ||
return; | ||
@@ -247,18 +322,23 @@ } | ||
get http() { | ||
return this.protocol === 'https:' ? _https2.default : _http2.default; | ||
_debugRequest() { | ||
if (this.options.agent) debug('proxy: %o', this.options.agent.options); | ||
debug('--> %s %s %O', this.options.method, this.url, this._redactedHeaders(this.options.headers)); | ||
} | ||
get url() { | ||
return `${this.protocol}//${this.host}${this.path}`; | ||
_debugResponse() { | ||
if (this.body) { | ||
debug('<-- %s %s %s\nHeaders: %O\nBody: %O', this.method, this.url, this.statusCode, this._redactedHeaders(this.headers), this.body); | ||
} else { | ||
debug('<-- %s %s %s\nHeaders: %O\nBody: %O', this.method, this.url, this.statusCode, this._redactedHeaders(this.headers), this.body); | ||
} | ||
} | ||
performRequest() { | ||
_performRequest() { | ||
return new Promise((resolve, reject) => { | ||
this.request = this.http.request(this, resolve); | ||
this.request = this._http.request(this.options, resolve); | ||
this.request.on('error', reject); | ||
if (_isStream2.default.readable(this.requestBody)) { | ||
this.requestBody.pipe(this.request); | ||
if (this.options.body && _isStream2.default.readable(this.options.body)) { | ||
this.options.body.pipe(this.request); | ||
} else { | ||
this.request.end(this.requestBody); | ||
this.request.end(this.options.body); | ||
} | ||
@@ -268,33 +348,56 @@ }); | ||
async parse(response) { | ||
let body = await concat(response); | ||
return response.headers['content-type'] === 'application/json' ? JSON.parse(body) : body; | ||
async _parse() { | ||
this.body = await concat(this.response); | ||
let json = this.headers['content-type'] === 'application/json'; | ||
if (json) this.body = JSON.parse(this.body); | ||
} | ||
parseBody(body) { | ||
_parseBody(body) { | ||
if (_isStream2.default.readable(body)) { | ||
this.requestBody = body; | ||
this.options.body = body; | ||
return; | ||
} | ||
if (!this.headers['Content-Type']) { | ||
this.headers['Content-Type'] = 'application/json'; | ||
if (!this.options.headers['content-type']) { | ||
this.options.headers['content-type'] = 'application/json'; | ||
} | ||
if (this.headers['Content-Type'] === 'application/json') { | ||
this.requestBody = JSON.stringify(body); | ||
if (this.options.headers['content-type'] === 'application/json') { | ||
this.options.body = JSON.stringify(body); | ||
} else { | ||
this.requestBody = body; | ||
this.options.body = body; | ||
} | ||
this.headers['Content-Length'] = Buffer.byteLength(this.requestBody).toString(); | ||
this.options.headers['content-length'] = Buffer.byteLength(this.options.body).toString(); | ||
} | ||
static async _getNextBody(http) { | ||
if (http.partial || !http.response.headers['next-range'] || !(http.body instanceof Array)) return http.body; | ||
let opts = { headers: {} }; | ||
opts = Object.assign(opts, http.options); | ||
opts.headers['range'] = http.response.headers['next-range']; | ||
let next = await this.get(http.url, opts); | ||
return http.body.concat(next); | ||
async _getNextRange() { | ||
this.options.headers['range'] = this.headers['next-range']; | ||
let prev = this.body; | ||
await this._request(); | ||
this.body = prev.concat(this.body); | ||
} | ||
_redactedHeaders(headers) { | ||
headers = { ...headers }; | ||
if (headers.authorization) headers.authorization = '[REDACTED]'; | ||
return headers; | ||
} | ||
get _http() { | ||
return this.secure ? _https2.default : _http2.default; | ||
} | ||
get _responseOK() { | ||
if (!this.response) return false; | ||
return this.statusCode >= 200 && this.statusCode < 300; | ||
} | ||
get _responseRedirect() { | ||
if (!this.response) return false; | ||
return this.statusCode >= 300 && this.statusCode < 400; | ||
} | ||
get _shouldParseResponseBody() { | ||
return !this._responseOK || !this.options.raw && this._responseOK; | ||
} | ||
_wait(ms) { | ||
@@ -308,14 +411,14 @@ return new Promise(resolve => setTimeout(resolve, ms)); | ||
constructor(http, body) { | ||
constructor(http) { | ||
let message; | ||
if (typeof body === 'string' || typeof body.message === 'string') message = body.message || body;else message = _util2.default.inspect(body); | ||
super(`HTTP Error ${http.response.statusCode} for ${http.method} ${http.url}\n${message}`); | ||
if (typeof http.body === 'string' || typeof http.body.message === 'string') message = http.body.message || http.body;else message = _util2.default.inspect(http.body); | ||
super(`HTTP Error ${http.statusCode} for ${http.method} ${http.url}\n${message}`); | ||
this.__httpcall = true; | ||
this.statusCode = http.response.statusCode; | ||
this.statusCode = http.statusCode; | ||
this.http = http; | ||
this.body = body; | ||
this.body = http.body; | ||
} | ||
} | ||
exports.HTTPError = HTTPError; // commonjs helpers | ||
exports.HTTPError = HTTPError; // common/s helpers | ||
@@ -322,0 +425,0 @@ function get(url, options = {}) { |
{ | ||
"name": "http-call", | ||
"description": "make http requests", | ||
"version": "2.1.6", | ||
"version": "3.0.0", | ||
"author": "Jeff Dickey @dickeyxxx", | ||
"bugs": "https://github.com/dickeyxxx/http-call/issues", | ||
"dependencies": { | ||
"debug": "^3.0.0", | ||
"debug": "^3.0.1", | ||
"is-retry-allowed": "^1.1.0", | ||
@@ -14,11 +14,12 @@ "is-stream": "^1.1.0", | ||
"devDependencies": { | ||
"babel-cli": "6.24.1", | ||
"babel-core": "^6.25.0", | ||
"babel-cli": "^6.26.0", | ||
"babel-core": "^6.26.0", | ||
"babel-eslint": "7.2.3", | ||
"babel-jest": "20.0.3", | ||
"babel-plugin-syntax-object-rest-spread": "^6.13.0", | ||
"babel-plugin-transform-class-properties": "6.24.1", | ||
"babel-plugin-transform-es2015-modules-commonjs": "6.24.1", | ||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", | ||
"babel-plugin-transform-flow-strip-types": "6.22.0", | ||
"flow-bin": "^0.52.0", | ||
"flow-copy-source": "^1.2.0", | ||
"flow-bin": "0.52.0", | ||
"flow-copy-source": "^1.2.1", | ||
"flow-typed": "^2.1.5", | ||
@@ -33,3 +34,3 @@ "jest": "20.0.4", | ||
"engines": { | ||
"node": ">=7.6.0" | ||
"node": ">=8.3.0" | ||
}, | ||
@@ -40,7 +41,2 @@ "files": [ | ||
"homepage": "https://github.com/dickeyxxx/http-call", | ||
"jest": { | ||
"testEnvironment": "node", | ||
"coverageDirectory": "./coverage/", | ||
"collectCoverage": true | ||
}, | ||
"keywords": [ | ||
@@ -72,3 +68,4 @@ "http", | ||
"$Diff", | ||
"$Shape" | ||
"$Shape", | ||
"Class" | ||
], | ||
@@ -75,0 +72,0 @@ "ignore": [ |
Sorry, the diff of this file is not supported yet
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
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
29364
445
17
Updateddebug@^3.0.1