balena-request
Advanced tools
Comparing version 11.0.0-11-x-e4271637472eba7072f79301f00c6c2e5ff87f20 to 11.0.0-11-x-fc5adc4fbc8d4f9d7edeeab4a72f2838773c029d
@@ -17,2 +17,11 @@ "use strict"; | ||
*/ | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -35,3 +44,3 @@ exports.estimate = void 0; | ||
* @param {Function} [onState] - on state callback (state) | ||
* @returns {ReadableStream} progress stream | ||
* @returns {NodeJS.ReadableStream} progress stream | ||
* | ||
@@ -69,3 +78,3 @@ * @example | ||
* | ||
* @returns {() => Promise<ReadableStream>} request stream | ||
* @returns {(options) => Promise<NodeJS.ReadableStream>} request stream | ||
* | ||
@@ -80,24 +89,25 @@ * @example | ||
return function (options) { | ||
if (requestAsync == null) { | ||
requestAsync = utils.getRequestAsync(); | ||
} | ||
options.gzip = false; | ||
options.headers['Accept-Encoding'] = 'gzip, deflate'; | ||
let reader = null; | ||
if (options.signal != null) { | ||
options.signal.addEventListener('abort', function () { | ||
// We need to react to Abort events at this level, because otherwise our | ||
// reader locks the stream and lower-level cancellation causes error. | ||
if (reader) { | ||
reader.cancel().catch(function () { | ||
// ignore | ||
}); | ||
return reader.releaseLock(); | ||
} | ||
}, { once: true }); | ||
} | ||
return requestAsync(options).then(function (response) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (requestAsync == null) { | ||
requestAsync = utils.getRequestAsync(); | ||
} | ||
options.gzip = false; | ||
options.headers['Accept-Encoding'] = 'gzip, deflate'; | ||
let reader = null; | ||
if (options.signal != null) { | ||
options.signal.addEventListener('abort', function () { | ||
// We need to react to Abort events at this level, because otherwise our | ||
// reader locks the stream and lower-level cancellation causes error. | ||
if (reader) { | ||
reader.cancel().catch(function () { | ||
// ignore | ||
}); | ||
return reader.releaseLock(); | ||
} | ||
}, { once: true }); | ||
} | ||
const response = yield requestAsync(options); | ||
let responseStream; | ||
const output = new stream.PassThrough(); | ||
// @ts-ignore | ||
// @ts-expect-error | ||
output.response = response; | ||
@@ -104,0 +114,0 @@ const responseLength = utils.getResponseLength(response); |
"use strict"; | ||
/* | ||
* decaffeinate suggestions: | ||
* DS102: Remove unnecessary code created because of implicit returns | ||
* DS201: Simplify complex destructure assignments | ||
* DS207: Consider shorter variations of null checks | ||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md | ||
*/ | ||
/* | ||
Copyright 2016-2020 Balena Ltd. | ||
@@ -24,2 +17,11 @@ | ||
*/ | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -29,5 +31,3 @@ /** | ||
*/ | ||
const Promise = require("bluebird"); | ||
const urlLib = require("url"); | ||
const rindle = require("rindle"); | ||
const fetchReadableStream = require("fetch-readablestream"); | ||
@@ -55,41 +55,33 @@ const errors = require("balena-errors"); | ||
const prepareOptions = function (options) { | ||
if (options == null) { | ||
options = {}; | ||
} | ||
options = Object.assign({ | ||
method: 'GET', | ||
json: true, | ||
strictSSL: true, | ||
headers: {}, | ||
sendToken: true, | ||
refreshToken: true, | ||
retries, | ||
}, options); | ||
if (options.uri) { | ||
options.url = options.uri; | ||
delete options.uri; | ||
} | ||
if (urlLib.parse(options.url).protocol != null) { | ||
delete options.baseUrl; | ||
} | ||
return Promise.try(function () { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (options == null) { | ||
options = {}; | ||
} | ||
options = Object.assign({ | ||
method: 'GET', | ||
json: true, | ||
strictSSL: true, | ||
headers: {}, | ||
sendToken: true, | ||
refreshToken: true, | ||
retries, | ||
}, options); | ||
if (options.uri) { | ||
options.url = options.uri; | ||
delete options.uri; | ||
} | ||
if (urlLib.parse(options.url).protocol != null) { | ||
delete options.baseUrl; | ||
} | ||
// Only refresh if we have balena-auth, we're going to use it to send a | ||
// token, and we haven't opted out of refresh | ||
if (!(auth != null && options.sendToken && options.refreshToken)) { | ||
return; | ||
} | ||
return utils.shouldRefreshKey(auth).then(function (shouldRefreshKey) { | ||
if (!shouldRefreshKey) { | ||
return; | ||
if (auth != null && options.sendToken && options.refreshToken) { | ||
const shouldRefreshKey = yield utils.shouldRefreshKey(auth); | ||
if (shouldRefreshKey) { | ||
yield exports.refreshToken(options); | ||
} | ||
return exports.refreshToken(options); | ||
}); | ||
}) | ||
.then(function () { | ||
if (options.sendToken) { | ||
// @ts-expect-error | ||
return utils.getAuthorizationHeader(auth); | ||
} | ||
}) | ||
.then(function (authorizationHeader) { | ||
const authorizationHeader = options.sendToken | ||
? yield utils.getAuthorizationHeader(auth) | ||
: undefined; | ||
if (authorizationHeader != null) { | ||
@@ -115,15 +107,6 @@ options.headers.Authorization = authorizationHeader; | ||
const interceptResponseError = (responseError) => interceptResponseOrError(Promise.reject(responseError)); | ||
var interceptRequestOrError = (initialPromise) => Promise.resolve(exports.interceptors.reduce(function (promise, { request, requestError }) { | ||
if (request != null || requestError != null) { | ||
return promise.then(request, requestError); | ||
} | ||
else { | ||
return promise; | ||
} | ||
}, initialPromise)); | ||
var interceptResponseOrError = function (initialPromise) { | ||
interceptors = exports.interceptors.slice().reverse(); | ||
return Promise.resolve(interceptors.reduce(function (promise, { response, responseError }) { | ||
if (response != null || responseError != null) { | ||
return promise.then(response, responseError); | ||
var interceptRequestOrError = (initialPromise) => __awaiter(this, void 0, void 0, function* () { | ||
return exports.interceptors.reduce(function (promise, { request, requestError }) { | ||
if (request != null || requestError != null) { | ||
return promise.then(request, requestError); | ||
} | ||
@@ -133,3 +116,16 @@ else { | ||
} | ||
}, initialPromise)); | ||
}, initialPromise); | ||
}); | ||
var interceptResponseOrError = function (initialPromise) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
interceptors = exports.interceptors.slice().reverse(); | ||
return interceptors.reduce(function (promise, { response, responseError }) { | ||
if (response != null || responseError != null) { | ||
return promise.then(response, responseError); | ||
} | ||
else { | ||
return promise; | ||
} | ||
}, initialPromise); | ||
}); | ||
}; | ||
@@ -178,24 +174,31 @@ /** | ||
exports.send = function (options) { | ||
// Only set the default timeout when doing a normal HTTP | ||
// request and not also when streaming since in the latter | ||
// case we might cause unnecessary ESOCKETTIMEDOUT errors. | ||
if (options.timeout == null) { | ||
options.timeout = 59000; | ||
} | ||
return prepareOptions(options) | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then((opts) => requestAsync(opts).catch(function (error) { | ||
error.requestOptions = opts; | ||
throw error; | ||
})) | ||
.then((response) => utils.getBody(response, options.responseFormat).then(function (body) { | ||
response = Object.assign({}, response, { body }); | ||
if (utils.isErrorCode(response.statusCode)) { | ||
const responseError = utils.getErrorMessageFromResponse(response); | ||
debugRequest(options, response); | ||
throw new errors.BalenaRequestError(responseError, response.statusCode, options); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// Only set the default timeout when doing a normal HTTP | ||
// request and not also when streaming since in the latter | ||
// case we might cause unnecessary ESOCKETTIMEDOUT errors. | ||
if (options.timeout == null) { | ||
options.timeout = 59000; | ||
} | ||
return response; | ||
})) | ||
.then(interceptResponse, interceptResponseError); | ||
return prepareOptions(options) | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then((opts) => __awaiter(this, void 0, void 0, function* () { | ||
let response; | ||
try { | ||
response = yield requestAsync(opts); | ||
} | ||
catch (err) { | ||
err.requestOptions = opts; | ||
throw err; | ||
} | ||
const body = yield utils.getBody(response, options.responseFormat); | ||
response = Object.assign(Object.assign({}, response), { body }); | ||
if (utils.isErrorCode(response.statusCode)) { | ||
const responseError = utils.getErrorMessageFromResponse(response); | ||
debugRequest(options, response); | ||
throw new errors.BalenaRequestError(responseError, response.statusCode, options); | ||
} | ||
return response; | ||
})) | ||
.then(interceptResponse, interceptResponseError); | ||
}); | ||
}; | ||
@@ -227,3 +230,3 @@ /** | ||
* | ||
* @returns {Promise<ReadableStream>} response | ||
* @returns {Promise<NodeJS.ReadableStream>} response | ||
* | ||
@@ -245,4 +248,4 @@ * @example | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then(progress.estimate(requestStream, isBrowser)) | ||
.then(function (download) { | ||
.then((opts) => __awaiter(this, void 0, void 0, function* () { | ||
const download = yield progress.estimate(requestStream, isBrowser)(opts); | ||
// @ts-expect-error | ||
@@ -255,14 +258,21 @@ if (!utils.isErrorCode(download.response.statusCode)) { | ||
} | ||
// If status code is an error code, interpret | ||
// the body of the request as an error. | ||
return rindle.extract(download).then(function (data) { | ||
const responseError = data || 'The request was unsuccessful'; | ||
// @ts-expect-error | ||
debugRequest(options, download.response); | ||
// @ts-expect-error | ||
throw new errors.BalenaRequestError(responseError, | ||
// @ts-expect-error | ||
download.response.statusCode); | ||
// If status code is an error code, interpret the body of the request as an error. | ||
const chunks = []; | ||
download.on('data', function (chunk) { | ||
chunks.push(chunk); | ||
}); | ||
}) | ||
yield new Promise((resolve, reject) => { | ||
download.on('error', reject); | ||
download.on('close', resolve); | ||
download.on('end', resolve); | ||
download.on('done', resolve); | ||
}); | ||
const responseError = chunks.join() || 'The request was unsuccessful'; | ||
// @ts-expect-error | ||
debugRequest(options, download.response); | ||
// @ts-expect-error | ||
throw new errors.BalenaRequestError(responseError, | ||
// @ts-expect-error | ||
download.response.statusCode); | ||
})) | ||
.then(interceptResponse, interceptResponseError); | ||
@@ -328,3 +338,3 @@ }; | ||
* | ||
* @returns {String} token - new token | ||
* @returns {Promise<String>} token - new token | ||
* | ||
@@ -336,25 +346,29 @@ * @example | ||
exports.refreshToken = function ({ baseUrl }) { | ||
// Only refresh if we have balena-auth | ||
if (auth == null) { | ||
throw new Error('Auth module not provided in initializer'); | ||
} | ||
return exports | ||
.send({ | ||
url: '/whoami', | ||
baseUrl, | ||
refreshToken: false, | ||
}) | ||
.catch({ | ||
code: 'BalenaRequestError', | ||
statusCode: 401, | ||
}, () => auth | ||
.getKey() | ||
.tap(auth.removeKey) | ||
.then(function (key) { | ||
throw new errors.BalenaExpiredToken(key); | ||
})) | ||
.get('body') | ||
.tap(auth.setKey); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// Only refresh if we have balena-auth | ||
if (auth == null) { | ||
throw new Error('Auth module not provided in initializer'); | ||
} | ||
let response; | ||
try { | ||
response = yield exports.send({ | ||
url: '/whoami', | ||
baseUrl, | ||
refreshToken: false, | ||
}); | ||
} | ||
catch (err) { | ||
if (err.code === 'BalenaRequestError' && err.statusCode === 401) { | ||
const expiredKey = yield auth.getKey(); | ||
yield auth.removeKey(); | ||
throw new errors.BalenaExpiredToken(expiredKey); | ||
} | ||
throw err; | ||
} | ||
const refreshedKey = response.body; | ||
yield auth.setKey(refreshedKey); | ||
return refreshedKey; | ||
}); | ||
}; | ||
return exports; | ||
}; |
"use strict"; | ||
/* | ||
* decaffeinate suggestions: | ||
* DS101: Remove unnecessary use of Array.from | ||
* DS102: Remove unnecessary code created because of implicit returns | ||
* DS205: Consider reworking code to avoid use of IIFEs | ||
* DS207: Consider shorter variations of null checks | ||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md | ||
*/ | ||
/* | ||
Copyright 2016-2020 Balena Ltd. | ||
@@ -25,5 +17,13 @@ | ||
*/ | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getRequestAsync = exports.getBody = exports.debugRequest = exports.getResponseLength = exports.isResponseCompressed = exports.isErrorCode = exports.getErrorMessageFromResponse = exports.getAuthorizationHeader = exports.shouldRefreshKey = exports.TOKEN_REFRESH_INTERVAL = void 0; | ||
const Promise = require("bluebird"); | ||
const { fetch: normalFetch, Headers: HeadersPonyfill, } = require('fetch-ponyfill')({ Promise }); | ||
@@ -57,14 +57,14 @@ const urlLib = require("url"); | ||
function shouldRefreshKey(auth) { | ||
return auth.hasKey().then(function (hasKey) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const hasKey = yield auth.hasKey(); | ||
if (!hasKey) { | ||
return false; | ||
} | ||
return auth.getType().then(function (type) { | ||
if (type !== token_1.TokenType.JWT) { | ||
return false; | ||
} | ||
return auth.getAge().then((age) => | ||
// @ts-ignore | ||
age >= exports.TOKEN_REFRESH_INTERVAL); | ||
}); | ||
const type = yield auth.getType(); | ||
if (type !== token_1.TokenType.JWT) { | ||
return false; | ||
} | ||
const age = yield auth.getAge(); | ||
// @ts-expect-error | ||
return age >= exports.TOKEN_REFRESH_INTERVAL; | ||
}); | ||
@@ -82,3 +82,3 @@ } | ||
* @param {import('balena-auth').default} auth - an instance of `balena-auth` | ||
* @returns {Promise<String>} authorization header | ||
* @returns {Promise<string | undefined>} authorization header | ||
* | ||
@@ -90,13 +90,15 @@ * @example | ||
*/ | ||
exports.getAuthorizationHeader = Promise.method(function (auth) { | ||
if (auth == null) { | ||
return; | ||
} | ||
return auth.hasKey().then(function (hasKey) { | ||
exports.getAuthorizationHeader = function (auth) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (auth == null) { | ||
return; | ||
} | ||
const hasKey = yield auth.hasKey(); | ||
if (!hasKey) { | ||
return; | ||
} | ||
return auth.getKey().then((key) => `Bearer ${key}`); | ||
const key = yield auth.getKey(); | ||
return `Bearer ${key}`; | ||
}); | ||
}); | ||
}; | ||
/** | ||
@@ -119,6 +121,7 @@ * @summary Get error message from response | ||
function getErrorMessageFromResponse(response) { | ||
var _a; | ||
if (!response.body) { | ||
return 'The request was unsuccessful'; | ||
} | ||
const errorText = response.body.error != null ? response.body.error.text : undefined; | ||
const errorText = (_a = response.body.error) === null || _a === void 0 ? void 0 : _a.text; | ||
if (errorText != null) { | ||
@@ -303,4 +306,3 @@ return errorText; | ||
function getBody(response, responseFormat) { | ||
// wrap in Bluebird promise for extra methods | ||
return Promise.try(function () { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (responseFormat === 'none') { | ||
@@ -311,6 +313,3 @@ return null; | ||
if (responseFormat === 'blob' || | ||
(responseFormat == null && | ||
(contentType != null | ||
? contentType.includes('binary/octet-stream') | ||
: undefined))) { | ||
(responseFormat == null && (contentType === null || contentType === void 0 ? void 0 : contentType.includes('binary/octet-stream')))) { | ||
// this is according to the standard | ||
@@ -321,5 +320,5 @@ if (typeof response.blob === 'function') { | ||
// https://github.com/bitinn/node-fetch/blob/master/lib/body.js#L66 | ||
// @ts-ignore | ||
// @ts-expect-error | ||
if (typeof response.buffer === 'function') { | ||
// @ts-ignore | ||
// @ts-expect-error | ||
return response.buffer(); | ||
@@ -330,6 +329,3 @@ } | ||
if (responseFormat === 'json' || | ||
(responseFormat == null && | ||
(contentType != null | ||
? contentType.includes('application/json') | ||
: undefined))) { | ||
(responseFormat == null && (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/json')))) { | ||
return response.json(); | ||
@@ -346,44 +342,53 @@ } | ||
var requestAsync = function (fetch, options, retriesRemaining) { | ||
const [url, opts] = Array.from(processRequestOptions(options)); | ||
if (retriesRemaining == null) { | ||
retriesRemaining = opts.retries; | ||
} | ||
// When streaming, prefer using the native Headers object if available | ||
if (fetch !== normalFetch && typeof Headers === 'function') { | ||
// Edge's Headers(args) ctor doesn't work as expected when passed in a headers object | ||
// from fetch-ponyfill, treating it as a plain object instead of using the iterator symbol. | ||
// As a result when fetch-readablestream uses the native fetch on Edge, the headers sent | ||
// to the server only contain a `map` property and not the actual headers that we want. | ||
const nativeHeaders = new Headers(); | ||
opts.headers.forEach((value, name) => nativeHeaders.append(name, value)); | ||
opts.headers = nativeHeaders; | ||
} | ||
const requestTime = Date.now(); | ||
let p = fetch(url, opts); | ||
if (opts.timeout && IS_BROWSER) { | ||
p = p.timeout(opts.timeout); | ||
} | ||
p = p.then(function (response) { | ||
if (opts.signal) { | ||
handleAbortIfNotSupported(opts.signal, response); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const [url, opts] = processRequestOptions(options); | ||
if (retriesRemaining == null) { | ||
retriesRemaining = opts.retries; | ||
} | ||
const responseTime = Date.now(); | ||
response.duration = responseTime - requestTime; | ||
response.statusCode = response.status; | ||
response.request = { | ||
headers: options.headers, | ||
uri: urlLib.parse(url), | ||
}; | ||
return response; | ||
// When streaming, prefer using the native Headers object if available | ||
if (fetch !== normalFetch && typeof Headers === 'function') { | ||
// Edge's Headers(args) ctor doesn't work as expected when passed in a headers object | ||
// from fetch-ponyfill, treating it as a plain object instead of using the iterator symbol. | ||
// As a result when fetch-readablestream uses the native fetch on Edge, the headers sent | ||
// to the server only contain a `map` property and not the actual headers that we want. | ||
const nativeHeaders = new Headers(); | ||
opts.headers.forEach((value, name) => nativeHeaders.append(name, value)); | ||
opts.headers = nativeHeaders; | ||
} | ||
try { | ||
const requestTime = Date.now(); | ||
let p = fetch(url, opts); | ||
if (opts.timeout && IS_BROWSER) { | ||
p = new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
reject(new Error('request timed out')); | ||
}, opts.timeout); | ||
p.then(resolve, reject); | ||
}); | ||
} | ||
const response = yield p; | ||
if (opts.signal) { | ||
handleAbortIfNotSupported(opts.signal, response); | ||
} | ||
const responseTime = Date.now(); | ||
response.duration = responseTime - requestTime; | ||
response.statusCode = response.status; | ||
response.request = { | ||
headers: options.headers, | ||
uri: urlLib.parse(url), | ||
}; | ||
return response; | ||
} | ||
catch (err) { | ||
if (retriesRemaining > 0) { | ||
return yield requestAsync(fetch, options, retriesRemaining - 1); | ||
} | ||
throw err; | ||
} | ||
}); | ||
if (retriesRemaining > 0) { | ||
return p.catch(() => requestAsync(fetch, options, retriesRemaining - 1)); | ||
} | ||
else { | ||
return p; | ||
} | ||
}; | ||
var handleAbortIfNotSupported = function (signal, response) { | ||
const emulateAbort = (() => { | ||
if (response.body != null ? response.body.cancel : undefined) { | ||
var _a, _b; | ||
if ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel) { | ||
// We have an XHR-emulated stream - cancel kills the underlying XHR | ||
@@ -395,3 +400,3 @@ // Context: https://github.com/jonnyreeves/fetch-readablestream/issues/6 | ||
} | ||
else if (response.body != null ? response.body.destroy : undefined) { | ||
else if ((_b = response.body) === null || _b === void 0 ? void 0 : _b.destroy) { | ||
// We have a Node stream - destroy kills the stream, and seems to kill | ||
@@ -398,0 +403,0 @@ // the underlying connection (hard to confirm - but it definitely stops streaming) |
@@ -10,2 +10,7 @@ # Change Log | ||
* Update fetch-ponyfill to 6.x [Pagan Gazzard] | ||
* Remove rindle dependency [Pagan Gazzard] | ||
* Update balena-auth to 4.x [Pagan Gazzard] | ||
* Drop support for nodejs < 10 [Pagan Gazzard] | ||
* Switch to returning native promises [Pagan Gazzard] | ||
* Convert to type checked javascript [Pagan Gazzard] | ||
@@ -12,0 +17,0 @@ |
@@ -35,3 +35,3 @@ /* | ||
* @param {Function} [onState] - on state callback (state) | ||
* @returns {ReadableStream} progress stream | ||
* @returns {NodeJS.ReadableStream} progress stream | ||
* | ||
@@ -73,3 +73,3 @@ * @example | ||
* | ||
* @returns {() => Promise<ReadableStream>} request stream | ||
* @returns {(options) => Promise<NodeJS.ReadableStream>} request stream | ||
* | ||
@@ -83,3 +83,3 @@ * @example | ||
export function estimate(requestAsync, isBrowser) { | ||
return function (options) { | ||
return async function (options) { | ||
if (requestAsync == null) { | ||
@@ -111,46 +111,46 @@ requestAsync = utils.getRequestAsync(); | ||
return requestAsync(options).then(function (response) { | ||
let responseStream; | ||
const output = new stream.PassThrough(); | ||
// @ts-ignore | ||
output.response = response; | ||
const response = await requestAsync(options); | ||
const responseLength = utils.getResponseLength(response); | ||
const total = responseLength.uncompressed || responseLength.compressed; | ||
let responseStream; | ||
const output = new stream.PassThrough(); | ||
// @ts-expect-error | ||
output.response = response; | ||
if (response.body.getReader) { | ||
// Convert browser (WHATWG) streams to Node streams | ||
responseStream = webStreams.toNodeReadable(response.body); | ||
reader = responseStream._reader; | ||
} else { | ||
responseStream = response.body; | ||
} | ||
const responseLength = utils.getResponseLength(response); | ||
const total = responseLength.uncompressed || responseLength.compressed; | ||
const progressStream = getProgressStream(total, (state) => | ||
output.emit('progress', state), | ||
); | ||
if (response.body.getReader) { | ||
// Convert browser (WHATWG) streams to Node streams | ||
responseStream = webStreams.toNodeReadable(response.body); | ||
reader = responseStream._reader; | ||
} else { | ||
responseStream = response.body; | ||
} | ||
if (!isBrowser && utils.isResponseCompressed(response)) { | ||
const gunzip = zlib.createGunzip(); | ||
const progressStream = getProgressStream(total, (state) => | ||
output.emit('progress', state), | ||
); | ||
// Uncompress after or before piping through progress | ||
// depending on the response length available to us | ||
if ( | ||
responseLength.compressed != null && | ||
responseLength.uncompressed == null | ||
) { | ||
responseStream.pipe(progressStream).pipe(gunzip).pipe(output); | ||
} else { | ||
responseStream.pipe(gunzip).pipe(progressStream).pipe(output); | ||
} | ||
if (!isBrowser && utils.isResponseCompressed(response)) { | ||
const gunzip = zlib.createGunzip(); | ||
// Uncompress after or before piping through progress | ||
// depending on the response length available to us | ||
if ( | ||
responseLength.compressed != null && | ||
responseLength.uncompressed == null | ||
) { | ||
responseStream.pipe(progressStream).pipe(gunzip).pipe(output); | ||
} else { | ||
responseStream.pipe(progressStream).pipe(output); | ||
responseStream.pipe(gunzip).pipe(progressStream).pipe(output); | ||
} | ||
} else { | ||
responseStream.pipe(progressStream).pipe(output); | ||
} | ||
// Stream any request errors on downstream | ||
responseStream.on('error', (e) => output.emit('error', e)); | ||
// Stream any request errors on downstream | ||
responseStream.on('error', (e) => output.emit('error', e)); | ||
return output; | ||
}); | ||
return output; | ||
}; | ||
} |
/* | ||
* decaffeinate suggestions: | ||
* DS102: Remove unnecessary code created because of implicit returns | ||
* DS201: Simplify complex destructure assignments | ||
* DS207: Consider shorter variations of null checks | ||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md | ||
*/ | ||
/* | ||
Copyright 2016-2020 Balena Ltd. | ||
@@ -28,5 +21,3 @@ | ||
import * as Promise from 'bluebird'; | ||
import * as urlLib from 'url'; | ||
import * as rindle from 'rindle'; | ||
import * as fetchReadableStream from 'fetch-readablestream'; | ||
@@ -63,3 +54,3 @@ import * as errors from 'balena-errors'; | ||
const prepareOptions = function (options) { | ||
const prepareOptions = async function (options) { | ||
if (options == null) { | ||
@@ -89,41 +80,30 @@ options = {}; | ||
return Promise.try(function () { | ||
// Only refresh if we have balena-auth, we're going to use it to send a | ||
// token, and we haven't opted out of refresh | ||
if (!(auth != null && options.sendToken && options.refreshToken)) { | ||
return; | ||
// Only refresh if we have balena-auth, we're going to use it to send a | ||
// token, and we haven't opted out of refresh | ||
if (auth != null && options.sendToken && options.refreshToken) { | ||
const shouldRefreshKey = await utils.shouldRefreshKey(auth); | ||
if (shouldRefreshKey) { | ||
await exports.refreshToken(options); | ||
} | ||
} | ||
const authorizationHeader = options.sendToken | ||
? await utils.getAuthorizationHeader(auth) | ||
: undefined; | ||
return utils.shouldRefreshKey(auth).then(function (shouldRefreshKey) { | ||
if (!shouldRefreshKey) { | ||
return; | ||
} | ||
if (authorizationHeader != null) { | ||
options.headers.Authorization = authorizationHeader; | ||
} | ||
return exports.refreshToken(options); | ||
}); | ||
}) | ||
.then(function () { | ||
if (options.sendToken) { | ||
// @ts-expect-error | ||
return utils.getAuthorizationHeader(auth); | ||
} | ||
}) | ||
.then(function (authorizationHeader) { | ||
if (authorizationHeader != null) { | ||
options.headers.Authorization = authorizationHeader; | ||
} | ||
if (typeof options.apiKey === 'string' && options.apiKey.length > 0) { | ||
// Using `request` qs object results in dollar signs, or other | ||
// special characters used to query our OData API, being escaped | ||
// and thus leading to all sort of weird error. | ||
// The workaround is to append the `apikey` query string manually | ||
// to prevent affecting the rest of the query strings. | ||
// See https://github.com/request/request/issues/2129 | ||
options.url += urlLib.parse(options.url).query != null ? '&' : '?'; | ||
options.url += `apikey=${options.apiKey}`; | ||
} | ||
if (typeof options.apiKey === 'string' && options.apiKey.length > 0) { | ||
// Using `request` qs object results in dollar signs, or other | ||
// special characters used to query our OData API, being escaped | ||
// and thus leading to all sort of weird error. | ||
// The workaround is to append the `apikey` query string manually | ||
// to prevent affecting the rest of the query strings. | ||
// See https://github.com/request/request/issues/2129 | ||
options.url += urlLib.parse(options.url).query != null ? '&' : '?'; | ||
options.url += `apikey=${options.apiKey}`; | ||
} | ||
return options; | ||
}); | ||
return options; | ||
}; | ||
@@ -143,28 +123,20 @@ | ||
var interceptRequestOrError = (initialPromise) => | ||
Promise.resolve( | ||
exports.interceptors.reduce(function ( | ||
promise, | ||
{ request, requestError }, | ||
) { | ||
if (request != null || requestError != null) { | ||
return promise.then(request, requestError); | ||
} else { | ||
return promise; | ||
} | ||
}, | ||
initialPromise), | ||
); | ||
var interceptRequestOrError = async (initialPromise) => | ||
exports.interceptors.reduce(function (promise, { request, requestError }) { | ||
if (request != null || requestError != null) { | ||
return promise.then(request, requestError); | ||
} else { | ||
return promise; | ||
} | ||
}, initialPromise); | ||
var interceptResponseOrError = function (initialPromise) { | ||
var interceptResponseOrError = async function (initialPromise) { | ||
interceptors = exports.interceptors.slice().reverse(); | ||
return Promise.resolve( | ||
interceptors.reduce(function (promise, { response, responseError }) { | ||
if (response != null || responseError != null) { | ||
return promise.then(response, responseError); | ||
} else { | ||
return promise; | ||
} | ||
}, initialPromise), | ||
); | ||
return interceptors.reduce(function (promise, { response, responseError }) { | ||
if (response != null || responseError != null) { | ||
return promise.then(response, responseError); | ||
} else { | ||
return promise; | ||
} | ||
}, initialPromise); | ||
}; | ||
@@ -213,3 +185,3 @@ | ||
*/ | ||
exports.send = function (options) { | ||
exports.send = async function (options) { | ||
// Only set the default timeout when doing a normal HTTP | ||
@@ -224,25 +196,26 @@ // request and not also when streaming since in the latter | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then((opts) => | ||
requestAsync(opts).catch(function (error) { | ||
error.requestOptions = opts; | ||
throw error; | ||
}), | ||
) | ||
.then((response) => | ||
utils.getBody(response, options.responseFormat).then(function (body) { | ||
response = Object.assign({}, response, { body }); | ||
.then(async (opts) => { | ||
let response; | ||
try { | ||
response = await requestAsync(opts); | ||
} catch (err) { | ||
err.requestOptions = opts; | ||
throw err; | ||
} | ||
if (utils.isErrorCode(response.statusCode)) { | ||
const responseError = utils.getErrorMessageFromResponse(response); | ||
debugRequest(options, response); | ||
throw new errors.BalenaRequestError( | ||
responseError, | ||
response.statusCode, | ||
options, | ||
); | ||
} | ||
const body = await utils.getBody(response, options.responseFormat); | ||
response = { ...response, body }; | ||
return response; | ||
}), | ||
) | ||
if (utils.isErrorCode(response.statusCode)) { | ||
const responseError = utils.getErrorMessageFromResponse(response); | ||
debugRequest(options, response); | ||
throw new errors.BalenaRequestError( | ||
responseError, | ||
response.statusCode, | ||
options, | ||
); | ||
} | ||
return response; | ||
}) | ||
.then(interceptResponse, interceptResponseError); | ||
@@ -276,3 +249,3 @@ }; | ||
* | ||
* @returns {Promise<ReadableStream>} response | ||
* @returns {Promise<NodeJS.ReadableStream>} response | ||
* | ||
@@ -295,4 +268,7 @@ * @example | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then(progress.estimate(requestStream, isBrowser)) | ||
.then(function (download) { | ||
.then(async (opts) => { | ||
const download = await progress.estimate( | ||
requestStream, | ||
isBrowser, | ||
)(opts); | ||
// @ts-expect-error | ||
@@ -307,15 +283,22 @@ if (!utils.isErrorCode(download.response.statusCode)) { | ||
// If status code is an error code, interpret | ||
// the body of the request as an error. | ||
return rindle.extract(download).then(function (data) { | ||
const responseError = data || 'The request was unsuccessful'; | ||
// If status code is an error code, interpret the body of the request as an error. | ||
const chunks = []; | ||
download.on('data', function (chunk) { | ||
chunks.push(chunk); | ||
}); | ||
await new Promise((resolve, reject) => { | ||
download.on('error', reject); | ||
download.on('close', resolve); | ||
download.on('end', resolve); | ||
download.on('done', resolve); | ||
}); | ||
const responseError = chunks.join() || 'The request was unsuccessful'; | ||
// @ts-expect-error | ||
debugRequest(options, download.response); | ||
// @ts-expect-error | ||
throw new errors.BalenaRequestError( | ||
responseError, | ||
// @ts-expect-error | ||
debugRequest(options, download.response); | ||
// @ts-expect-error | ||
throw new errors.BalenaRequestError( | ||
responseError, | ||
// @ts-expect-error | ||
download.response.statusCode, | ||
); | ||
}); | ||
download.response.statusCode, | ||
); | ||
}) | ||
@@ -385,3 +368,3 @@ .then(interceptResponse, interceptResponseError); | ||
* | ||
* @returns {String} token - new token | ||
* @returns {Promise<String>} token - new token | ||
* | ||
@@ -393,3 +376,3 @@ * @example | ||
exports.refreshToken = function ({ baseUrl }) { | ||
exports.refreshToken = async function ({ baseUrl }) { | ||
// Only refresh if we have balena-auth | ||
@@ -400,23 +383,20 @@ if (auth == null) { | ||
return exports | ||
.send({ | ||
let response; | ||
try { | ||
response = await exports.send({ | ||
url: '/whoami', | ||
baseUrl, | ||
refreshToken: false, | ||
}) | ||
.catch( | ||
{ | ||
code: 'BalenaRequestError', | ||
statusCode: 401, | ||
}, | ||
() => | ||
auth | ||
.getKey() | ||
.tap(auth.removeKey) | ||
.then(function (key) { | ||
throw new errors.BalenaExpiredToken(key); | ||
}), | ||
) | ||
.get('body') | ||
.tap(auth.setKey); | ||
}); | ||
} catch (err) { | ||
if (err.code === 'BalenaRequestError' && err.statusCode === 401) { | ||
const expiredKey = await auth.getKey(); | ||
await auth.removeKey(); | ||
throw new errors.BalenaExpiredToken(expiredKey); | ||
} | ||
throw err; | ||
} | ||
const refreshedKey = response.body; | ||
await auth.setKey(refreshedKey); | ||
return refreshedKey; | ||
}; | ||
@@ -423,0 +403,0 @@ |
176
lib/utils.js
/* | ||
* decaffeinate suggestions: | ||
* DS101: Remove unnecessary use of Array.from | ||
* DS102: Remove unnecessary code created because of implicit returns | ||
* DS205: Consider reworking code to avoid use of IIFEs | ||
* DS207: Consider shorter variations of null checks | ||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md | ||
*/ | ||
/* | ||
Copyright 2016-2020 Balena Ltd. | ||
@@ -25,4 +17,2 @@ | ||
import * as Promise from 'bluebird'; | ||
const { | ||
@@ -62,18 +52,14 @@ fetch: normalFetch, | ||
*/ | ||
export function shouldRefreshKey(auth) { | ||
return auth.hasKey().then(function (hasKey) { | ||
if (!hasKey) { | ||
return false; | ||
} | ||
return auth.getType().then(function (type) { | ||
if (type !== TokenType.JWT) { | ||
return false; | ||
} | ||
return auth.getAge().then( | ||
(age) => | ||
// @ts-ignore | ||
age >= TOKEN_REFRESH_INTERVAL, | ||
); | ||
}); | ||
}); | ||
export async function shouldRefreshKey(auth) { | ||
const hasKey = await auth.hasKey(); | ||
if (!hasKey) { | ||
return false; | ||
} | ||
const type = await auth.getType(); | ||
if (type !== TokenType.JWT) { | ||
return false; | ||
} | ||
const age = await auth.getAge(); | ||
// @ts-expect-error | ||
return age >= TOKEN_REFRESH_INTERVAL; | ||
} | ||
@@ -90,3 +76,3 @@ | ||
* @param {import('balena-auth').default} auth - an instance of `balena-auth` | ||
* @returns {Promise<String>} authorization header | ||
* @returns {Promise<string | undefined>} authorization header | ||
* | ||
@@ -98,13 +84,13 @@ * @example | ||
*/ | ||
export let getAuthorizationHeader = Promise.method(function (auth) { | ||
export let getAuthorizationHeader = async function (auth) { | ||
if (auth == null) { | ||
return; | ||
} | ||
return auth.hasKey().then(function (hasKey) { | ||
if (!hasKey) { | ||
return; | ||
} | ||
return auth.getKey().then((key) => `Bearer ${key}`); | ||
}); | ||
}); | ||
const hasKey = await auth.hasKey(); | ||
if (!hasKey) { | ||
return; | ||
} | ||
const key = await auth.getKey(); | ||
return `Bearer ${key}`; | ||
}; | ||
@@ -132,4 +118,3 @@ /** | ||
const errorText = | ||
response.body.error != null ? response.body.error.text : undefined; | ||
const errorText = response.body.error?.text; | ||
if (errorText != null) { | ||
@@ -337,52 +322,43 @@ return errorText; | ||
*/ | ||
export function getBody(response, responseFormat) { | ||
// wrap in Bluebird promise for extra methods | ||
return Promise.try(function () { | ||
if (responseFormat === 'none') { | ||
return null; | ||
} | ||
export async function getBody(response, responseFormat) { | ||
if (responseFormat === 'none') { | ||
return null; | ||
} | ||
const contentType = response.headers.get('Content-Type'); | ||
const contentType = response.headers.get('Content-Type'); | ||
if ( | ||
responseFormat === 'blob' || | ||
(responseFormat == null && | ||
(contentType != null | ||
? contentType.includes('binary/octet-stream') | ||
: undefined)) | ||
) { | ||
// this is according to the standard | ||
if (typeof response.blob === 'function') { | ||
return response.blob(); | ||
} | ||
// https://github.com/bitinn/node-fetch/blob/master/lib/body.js#L66 | ||
// @ts-ignore | ||
if (typeof response.buffer === 'function') { | ||
// @ts-ignore | ||
return response.buffer(); | ||
} | ||
throw new Error( | ||
'This `fetch` implementation does not support decoding binary streams.', | ||
); | ||
if ( | ||
responseFormat === 'blob' || | ||
(responseFormat == null && contentType?.includes('binary/octet-stream')) | ||
) { | ||
// this is according to the standard | ||
if (typeof response.blob === 'function') { | ||
return response.blob(); | ||
} | ||
if ( | ||
responseFormat === 'json' || | ||
(responseFormat == null && | ||
(contentType != null | ||
? contentType.includes('application/json') | ||
: undefined)) | ||
) { | ||
return response.json(); | ||
// https://github.com/bitinn/node-fetch/blob/master/lib/body.js#L66 | ||
// @ts-expect-error | ||
if (typeof response.buffer === 'function') { | ||
// @ts-expect-error | ||
return response.buffer(); | ||
} | ||
throw new Error( | ||
'This `fetch` implementation does not support decoding binary streams.', | ||
); | ||
} | ||
if (responseFormat == null || responseFormat === 'text') { | ||
return response.text(); | ||
} | ||
if ( | ||
responseFormat === 'json' || | ||
(responseFormat == null && contentType?.includes('application/json')) | ||
) { | ||
return response.json(); | ||
} | ||
throw new errors.BalenaInvalidParameterError( | ||
'responseFormat', | ||
responseFormat, | ||
); | ||
}); | ||
if (responseFormat == null || responseFormat === 'text') { | ||
return response.text(); | ||
} | ||
throw new errors.BalenaInvalidParameterError( | ||
'responseFormat', | ||
responseFormat, | ||
); | ||
} | ||
@@ -392,4 +368,4 @@ | ||
var requestAsync = function (fetch, options, retriesRemaining) { | ||
const [url, opts] = Array.from(processRequestOptions(options)); | ||
var requestAsync = async function (fetch, options, retriesRemaining) { | ||
const [url, opts] = processRequestOptions(options); | ||
if (retriesRemaining == null) { | ||
@@ -410,9 +386,16 @@ retriesRemaining = opts.retries; | ||
const requestTime = Date.now(); | ||
let p = fetch(url, opts); | ||
if (opts.timeout && IS_BROWSER) { | ||
p = p.timeout(opts.timeout); | ||
} | ||
try { | ||
const requestTime = Date.now(); | ||
let p = fetch(url, opts); | ||
if (opts.timeout && IS_BROWSER) { | ||
p = new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
reject(new Error('request timed out')); | ||
}, opts.timeout); | ||
p.then(resolve, reject); | ||
}); | ||
} | ||
p = p.then(function (response) { | ||
const response = await p; | ||
if (opts.signal) { | ||
@@ -430,8 +413,7 @@ handleAbortIfNotSupported(opts.signal, response); | ||
return response; | ||
}); | ||
if (retriesRemaining > 0) { | ||
return p.catch(() => requestAsync(fetch, options, retriesRemaining - 1)); | ||
} else { | ||
return p; | ||
} catch (err) { | ||
if (retriesRemaining > 0) { | ||
return await requestAsync(fetch, options, retriesRemaining - 1); | ||
} | ||
throw err; | ||
} | ||
@@ -442,3 +424,3 @@ }; | ||
const emulateAbort = (() => { | ||
if (response.body != null ? response.body.cancel : undefined) { | ||
if (response.body?.cancel) { | ||
// We have an XHR-emulated stream - cancel kills the underlying XHR | ||
@@ -450,3 +432,3 @@ // Context: https://github.com/jonnyreeves/fetch-readablestream/issues/6 | ||
}); | ||
} else if (response.body != null ? response.body.destroy : undefined) { | ||
} else if (response.body?.destroy) { | ||
// We have a Node stream - destroy kills the stream, and seems to kill | ||
@@ -453,0 +435,0 @@ // the underlying connection (hard to confirm - but it definitely stops streaming) |
{ | ||
"name": "balena-request", | ||
"version": "11.0.0-11-x-e4271637472eba7072f79301f00c6c2e5ff87f20", | ||
"version": "11.0.0-11-x-fc5adc4fbc8d4f9d7edeeab4a72f2838773c029d", | ||
"description": "Balena HTTP client", | ||
@@ -36,4 +36,5 @@ "main": "build/request.js", | ||
"@balena/lint": "^5.1.0", | ||
"balena-auth": "^3.1.1", | ||
"coffee-script": "~1.12.7", | ||
"balena-auth": "^4.0.0", | ||
"bluebird": "^3.7.2", | ||
"coffeescript": "~1.12.7", | ||
"global-tunnel-ng": "2.1.0", | ||
@@ -49,2 +50,3 @@ "jsdoc-to-markdown": "^6.0.1", | ||
"resin-config-karma": "^1.0.4", | ||
"rindle": "^1.3.6", | ||
"temp": "^0.8.4", | ||
@@ -58,8 +60,6 @@ "timekeeper": "^1.0.0", | ||
"balena-errors": "^4.4.0", | ||
"bluebird": "^3.7.2", | ||
"fetch-ponyfill": "^4.1.0", | ||
"fetch-ponyfill": "^6.1.1", | ||
"fetch-readablestream": "^0.2.0", | ||
"progress-stream": "^2.0.0", | ||
"qs": "^6.9.4", | ||
"rindle": "^1.3.6" | ||
"qs": "^6.9.4" | ||
}, | ||
@@ -66,0 +66,0 @@ "peerDependencies": { |
@@ -11,3 +11,3 @@ { | ||
"sourceMap": false, | ||
"target": "es2018", | ||
"target": "es2015", | ||
"noUnusedParameters": true, | ||
@@ -14,0 +14,0 @@ "noUnusedLocals": true, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
148235
7
10
19
1879
+ Addedfetch-ponyfill@6.1.1(transitive)
+ Addedmath-intrinsics@1.1.0(transitive)
+ Addednode-fetch@2.6.13(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)
- Removedbluebird@^3.7.2
- Removedrindle@^1.3.6
- Removedencoding@0.1.13(transitive)
- Removedfetch-ponyfill@4.1.0(transitive)
- Removediconv-lite@0.6.3(transitive)
- Removedmath-intrinsics@1.0.0(transitive)
- Removednode-fetch@1.7.3(transitive)
- Removedrindle@1.3.6(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedstring-to-stream@1.1.1(transitive)
Updatedfetch-ponyfill@^6.1.1