balena-request
Advanced tools
Comparing version 10.0.9 to 11.0.0-11-x-0b9fa20803d50fba3fa44bb8c914db76f9dfaaf3
@@ -1,5 +0,4 @@ | ||
// Generated by CoffeeScript 1.12.7 | ||
"use strict"; | ||
/* | ||
Copyright 2016 Balena | ||
Copyright 2016-2020 Balena Ltd. | ||
@@ -10,3 +9,3 @@ Licensed under the Apache License, Version 2.0 (the "License"); | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
@@ -18,21 +17,13 @@ Unless required by applicable law or agreed to in writing, software | ||
limitations under the License. | ||
*/ | ||
var getProgressStream, progress, stream, utils, webStreams, zlib; | ||
webStreams = require('@balena/node-web-streams'); | ||
progress = require('progress-stream'); | ||
zlib = require('zlib'); | ||
stream = require('stream'); | ||
utils = require('./utils'); | ||
*/ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.estimate = void 0; | ||
const webStreams = require("@balena/node-web-streams"); | ||
const progress = require("progress-stream"); | ||
const zlib = require("zlib"); | ||
const stream = require("stream"); | ||
const utils = require("./utils"); | ||
/** | ||
* @module progress | ||
*/ | ||
/** | ||
@@ -45,3 +36,3 @@ * @summary Get progress stream | ||
* @param {Function} [onState] - on state callback (state) | ||
* @returns {Stream} progress stream | ||
* @returns {ReadableStream} progress stream | ||
* | ||
@@ -54,24 +45,22 @@ * @example | ||
*/ | ||
getProgressStream = function(total, onState) { | ||
var progressStream; | ||
progressStream = progress({ | ||
time: 500, | ||
length: total | ||
}); | ||
progressStream.on('progress', function(state) { | ||
if (state.length === 0) { | ||
return typeof onState === "function" ? onState(void 0) : void 0; | ||
} | ||
return typeof onState === "function" ? onState({ | ||
total: state.length, | ||
received: state.transferred, | ||
eta: state.eta, | ||
percentage: state.percentage | ||
}) : void 0; | ||
}); | ||
return progressStream; | ||
const getProgressStream = function (total, onState) { | ||
const progressStream = progress({ | ||
time: 500, | ||
length: total, | ||
}); | ||
progressStream.on('progress', function (state) { | ||
if (state.length === 0) { | ||
return typeof onState === 'function' ? onState(undefined) : undefined; | ||
} | ||
return typeof onState === 'function' | ||
? onState({ | ||
total: state.length, | ||
received: state.transferred, | ||
eta: state.eta, | ||
percentage: state.percentage, | ||
}) | ||
: undefined; | ||
}); | ||
return progressStream; | ||
}; | ||
/** | ||
@@ -82,62 +71,67 @@ * @summary Make a node request with progress | ||
* | ||
* @param {Object} options - request options | ||
* @returns {Promise<Stream>} request stream | ||
* @returns {(options) => Promise<ReadableStream>} request stream | ||
* | ||
* @example | ||
* progress.estimate(options).then (stream) -> | ||
* stream.pipe(fs.createWriteStream('foo/bar')) | ||
* stream.on 'progress', (state) -> | ||
* console.log(state) | ||
* stream.pipe(fs.createWriteStream('foo/bar')) | ||
* stream.on 'progress', (state) -> | ||
* console.log(state) | ||
*/ | ||
exports.estimate = function(requestAsync, isBrowser) { | ||
return function(options) { | ||
var reader; | ||
if (requestAsync == null) { | ||
requestAsync = utils.getRequestAsync(); | ||
} | ||
options.gzip = false; | ||
options.headers['Accept-Encoding'] = 'gzip, deflate'; | ||
reader = null; | ||
if (options.signal != null) { | ||
options.signal.addEventListener('abort', function() { | ||
if (reader) { | ||
reader.cancel()["catch"](function() {}); | ||
return reader.releaseLock(); | ||
function estimate(requestAsync, isBrowser) { | ||
// @ts-expect-error | ||
return async function (options) { | ||
if (requestAsync == null) { | ||
requestAsync = utils.getRequestAsync(); | ||
} | ||
}, { | ||
once: true | ||
}); | ||
} | ||
return requestAsync(options).then(function(response) { | ||
var gunzip, output, progressStream, responseLength, responseStream, total; | ||
output = new stream.PassThrough(); | ||
output.response = response; | ||
responseLength = utils.getResponseLength(response); | ||
total = responseLength.uncompressed || responseLength.compressed; | ||
if (response.body.getReader) { | ||
responseStream = webStreams.toNodeReadable(response.body); | ||
reader = responseStream._reader; | ||
} else { | ||
responseStream = response.body; | ||
} | ||
progressStream = getProgressStream(total, function(state) { | ||
return output.emit('progress', state); | ||
}); | ||
if (!isBrowser && utils.isResponseCompressed(response)) { | ||
gunzip = new zlib.createGunzip(); | ||
if ((responseLength.compressed != null) && (responseLength.uncompressed == null)) { | ||
responseStream.pipe(progressStream).pipe(gunzip).pipe(output); | ||
} else { | ||
responseStream.pipe(gunzip).pipe(progressStream).pipe(output); | ||
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 }); | ||
} | ||
} else { | ||
responseStream.pipe(progressStream).pipe(output); | ||
} | ||
responseStream.on('error', function(e) { | ||
return output.emit('error', e); | ||
}); | ||
return output; | ||
}); | ||
}; | ||
}; | ||
const response = await requestAsync(options); | ||
let responseStream; | ||
const output = new stream.PassThrough(); | ||
// @ts-expect-error | ||
output.response = response; | ||
const responseLength = utils.getResponseLength(response); | ||
const total = responseLength.uncompressed || responseLength.compressed; | ||
if (response.body.getReader) { | ||
// Convert browser (WHATWG) streams to Node streams | ||
responseStream = webStreams.toNodeReadable(response.body); | ||
reader = responseStream._reader; | ||
} | ||
else { | ||
responseStream = response.body; | ||
} | ||
const progressStream = getProgressStream(total, (state) => output.emit('progress', state)); | ||
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(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)); | ||
return output; | ||
}; | ||
} | ||
exports.estimate = estimate; |
@@ -1,5 +0,4 @@ | ||
// Generated by CoffeeScript 1.12.7 | ||
"use strict"; | ||
/* | ||
Copyright 2016 Balena | ||
Copyright 2016-2020 Balena Ltd. | ||
@@ -17,327 +16,323 @@ Licensed under the Apache License, Version 2.0 (the "License"); | ||
limitations under the License. | ||
*/ | ||
*/ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
/** | ||
* @module request | ||
*/ | ||
var Promise, errors, fetchReadableStream, getRequest, progress, rindle, urlLib, utils; | ||
Promise = require('bluebird'); | ||
urlLib = require('url'); | ||
rindle = require('rindle'); | ||
fetchReadableStream = require('fetch-readablestream'); | ||
errors = require('balena-errors'); | ||
utils = require('./utils'); | ||
progress = require('./progress'); | ||
module.exports = getRequest = function(arg) { | ||
var auth, debug, debugRequest, exports, interceptRequestError, interceptRequestOptions, interceptRequestOrError, interceptResponse, interceptResponseError, interceptResponseOrError, interceptors, isBrowser, prepareOptions, ref, ref1, ref2, ref3, ref4, requestAsync, requestBrowserStream, retries; | ||
ref = arg != null ? arg : {}, auth = ref.auth, debug = (ref1 = ref.debug) != null ? ref1 : false, retries = (ref2 = ref.retries) != null ? ref2 : 0, isBrowser = (ref3 = ref.isBrowser) != null ? ref3 : false, interceptors = (ref4 = ref.interceptors) != null ? ref4 : []; | ||
requestAsync = utils.getRequestAsync(); | ||
requestBrowserStream = utils.getRequestAsync(fetchReadableStream); | ||
debugRequest = !debug ? function() {} : utils.debugRequest; | ||
exports = {}; | ||
prepareOptions = function(options) { | ||
var baseUrl; | ||
if (options == null) { | ||
options = {}; | ||
} | ||
options = Object.assign({ | ||
method: 'GET', | ||
json: true, | ||
strictSSL: true, | ||
headers: {}, | ||
sendToken: true, | ||
refreshToken: true, | ||
retries: retries | ||
}, options); | ||
baseUrl = options.baseUrl; | ||
if (options.uri) { | ||
options.url = options.uri; | ||
delete options.uri; | ||
} | ||
if (urlLib.parse(options.url).protocol != null) { | ||
delete options.baseUrl; | ||
} | ||
return Promise["try"](function() { | ||
if (!((auth != null) && options.sendToken && options.refreshToken)) { | ||
return; | ||
} | ||
return utils.shouldRefreshKey(auth).then(function(shouldRefreshKey) { | ||
if (!shouldRefreshKey) { | ||
return; | ||
const urlLib = require("url"); | ||
const rindle = require("rindle"); | ||
const fetchReadableStream = require("fetch-readablestream"); | ||
const errors = require("balena-errors"); | ||
const utils = require("./utils"); | ||
const progress = require("./progress"); | ||
/** | ||
* @param {object} options | ||
* @param {import('balena-auth').default} options.auth | ||
* @param {boolean} options.debug | ||
* @param {number} options.retries | ||
* @param {boolean} options.isBrowser | ||
* @param {array} options.interceptors | ||
*/ | ||
module.exports = function getRequest({ auth, debug = false, retries = 0, isBrowser = false, interceptors = [], }) { | ||
const requestAsync = utils.getRequestAsync(); | ||
const requestBrowserStream = utils.getRequestAsync(fetchReadableStream); | ||
const debugRequest = !debug | ||
? function () { | ||
// noop | ||
} | ||
return exports.refreshToken(options); | ||
}); | ||
}).then(function() { | ||
if (options.sendToken) { | ||
return utils.getAuthorizationHeader(auth); | ||
} | ||
}).then(function(authorizationHeader) { | ||
if (authorizationHeader != null) { | ||
options.headers.Authorization = authorizationHeader; | ||
} | ||
if (typeof options.apiKey === 'string' && options.apiKey.length > 0) { | ||
options.url += urlLib.parse(options.url).query != null ? '&' : '?'; | ||
options.url += "apikey=" + options.apiKey; | ||
} | ||
return options; | ||
}); | ||
}; | ||
interceptRequestOptions = function(requestOptions) { | ||
return interceptRequestOrError(Promise.resolve(requestOptions)); | ||
}; | ||
interceptRequestError = function(requestError) { | ||
return interceptRequestOrError(Promise.reject(requestError)); | ||
}; | ||
interceptResponse = function(response) { | ||
return interceptResponseOrError(Promise.resolve(response)); | ||
}; | ||
interceptResponseError = function(responseError) { | ||
return interceptResponseOrError(Promise.reject(responseError)); | ||
}; | ||
interceptRequestOrError = function(initialPromise) { | ||
return Promise.resolve(exports.interceptors.reduce(function(promise, arg1) { | ||
var request, requestError; | ||
request = arg1.request, requestError = arg1.requestError; | ||
if ((request != null) || (requestError != null)) { | ||
return promise.then(request, requestError); | ||
} else { | ||
return promise; | ||
} | ||
}, initialPromise)); | ||
}; | ||
interceptResponseOrError = function(initialPromise) { | ||
interceptors = exports.interceptors.slice().reverse(); | ||
return Promise.resolve(interceptors.reduce(function(promise, arg1) { | ||
var response, responseError; | ||
response = arg1.response, responseError = arg1.responseError; | ||
if ((response != null) || (responseError != null)) { | ||
return promise.then(response, responseError); | ||
} else { | ||
return promise; | ||
} | ||
}, initialPromise)); | ||
}; | ||
/** | ||
* @summary Perform an HTTP request to balena | ||
* @function | ||
* @public | ||
* | ||
* @description | ||
* This function automatically handles authorization with balena. | ||
* | ||
* The module scans your environment for a saved session token. Alternatively, you may pass the `apiKey` option. Otherwise, the request is made anonymously. | ||
* | ||
* Requests can be aborted using an AbortController (with a polyfill like https://www.npmjs.com/package/abortcontroller-polyfill | ||
* if necessary). This is not well supported everywhere yet, is on a best-efforts basis, and should not be relied upon. | ||
* | ||
* @param {Object} options - options | ||
* @param {String} [options.method='GET'] - method | ||
* @param {String} options.url - relative url | ||
* @param {String} [options.apiKey] - api key | ||
* @param {String} [options.responseFormat] - explicit expected response format, | ||
* can be one of 'blob', 'json', 'text', 'none'. Defaults to sniffing the content-type | ||
* @param {AbortSignal} [options.signal] - a signal from an AbortController | ||
* @param {*} [options.body] - body | ||
* | ||
* @returns {Promise<Object>} response | ||
* | ||
* @example | ||
* request.send | ||
* method: 'GET' | ||
* baseUrl: 'https://api.balena-cloud.com' | ||
* url: '/foo' | ||
* .get('body') | ||
* | ||
* @example | ||
* request.send | ||
* method: 'POST' | ||
* baseUrl: 'https://api.balena-cloud.com' | ||
* url: '/bar' | ||
* data: | ||
* hello: 'world' | ||
* .get('body') | ||
*/ | ||
exports.send = function(options) { | ||
if (options == null) { | ||
options = {}; | ||
} | ||
if (options.timeout == null) { | ||
options.timeout = 59000; | ||
} | ||
return prepareOptions(options).then(interceptRequestOptions, interceptRequestError).then(function(options) { | ||
return requestAsync(options)["catch"](function(error) { | ||
error.requestOptions = options; | ||
throw error; | ||
}); | ||
}).then(function(response) { | ||
return utils.getBody(response, options.responseFormat).then(function(body) { | ||
var responseError; | ||
response = Object.assign({}, response, { | ||
body: body | ||
}); | ||
if (utils.isErrorCode(response.statusCode)) { | ||
responseError = utils.getErrorMessageFromResponse(response); | ||
debugRequest(options, response); | ||
throw new errors.BalenaRequestError(responseError, response.statusCode, options); | ||
: utils.debugRequest; | ||
const exports = {}; | ||
const prepareOptions = async function (options) { | ||
if (options == null) { | ||
options = {}; | ||
} | ||
return response; | ||
}); | ||
}).then(interceptResponse, interceptResponseError); | ||
}; | ||
/** | ||
* @summary Stream an HTTP response from balena. | ||
* @function | ||
* @public | ||
* | ||
* @description | ||
* This function emits a `progress` event, passing an object with the following properties: | ||
* | ||
* - `Number percent`: from 0 to 100. | ||
* - `Number total`: total bytes to be transmitted. | ||
* - `Number received`: number of bytes transmitted. | ||
* - `Number eta`: estimated remaining time, in seconds. | ||
* | ||
* The stream may also contain the following custom properties: | ||
* | ||
* - `String .mime`: Equals the value of the `Content-Type` HTTP header. | ||
* | ||
* See `request.send()` for an explanation on how this function handles authentication, and details | ||
* on how to abort requests. | ||
* | ||
* @param {Object} options - options | ||
* @param {String} [options.method='GET'] - method | ||
* @param {String} options.url - relative url | ||
* @param {*} [options.body] - body | ||
* | ||
* @returns {Promise<Stream>} response | ||
* | ||
* @example | ||
* request.stream | ||
* method: 'GET' | ||
* baseUrl: 'https://img.balena-cloud.com' | ||
* url: '/download/foo' | ||
* .then (stream) -> | ||
* stream.on 'progress', (state) -> | ||
* console.log(state) | ||
* | ||
* stream.pipe(fs.createWriteStream('/opt/download')) | ||
*/ | ||
exports.stream = function(options) { | ||
var requestStream; | ||
if (options == null) { | ||
options = {}; | ||
} | ||
requestStream = isBrowser ? requestBrowserStream : requestAsync; | ||
return prepareOptions(options).then(interceptRequestOptions, interceptRequestError).then(progress.estimate(requestStream, isBrowser)).then(function(download) { | ||
if (!utils.isErrorCode(download.response.statusCode)) { | ||
download.mime = download.response.headers.get('Content-Type'); | ||
return download; | ||
} | ||
return rindle.extract(download).then(function(data) { | ||
var responseError; | ||
responseError = data || 'The request was unsuccessful'; | ||
debugRequest(options, download.response); | ||
throw new errors.BalenaRequestError(responseError, download.response.statusCode); | ||
}); | ||
}).then(interceptResponse, interceptResponseError); | ||
}; | ||
/** | ||
* @summary Array of interceptors | ||
* @type {Interceptor[]} | ||
* @public | ||
* | ||
* @description | ||
* The current array of interceptors to use. Interceptors intercept requests made | ||
* by calls to `.stream()` and `.send()` (some of which are made internally) and | ||
* are executed in the order they appear in this array for requests, and in the | ||
* reverse order for responses. | ||
* | ||
* @example | ||
* request.interceptors.push( | ||
* requestError: (error) -> | ||
* console.log(error) | ||
* throw error | ||
* ) | ||
*/ | ||
exports.interceptors = interceptors; | ||
/** | ||
* @typedef Interceptor | ||
* @type {object} | ||
* | ||
* @description | ||
* An interceptor implements some set of the four interception hook callbacks. | ||
* To continue processing, each function should return a value or a promise that | ||
* successfully resolves to a value. | ||
* | ||
* To halt processing, each function should throw an error or return a promise that | ||
* rejects with an error. | ||
* | ||
* @property {function} [request] - Callback invoked before requests are made. Called with | ||
* the request options, should return (or resolve to) new request options, or throw/reject. | ||
* | ||
* @property {function} [response] - Callback invoked before responses are returned. Called with | ||
* the response, should return (or resolve to) a new response, or throw/reject. | ||
* | ||
* @property {function} [requestError] - Callback invoked if an error happens before a request. | ||
* Called with the error itself, caused by a preceeding request interceptor rejecting/throwing | ||
* an error for the request, or a failing in preflight token validation. Should return (or resolve | ||
* to) new request options, or throw/reject. | ||
* | ||
* @property {function} [responseError] - Callback invoked if an error happens in the response. | ||
* Called with the error itself, caused by a preceeding response interceptor rejecting/throwing | ||
* an error for the request, a network error, or an error response from the server. Should return | ||
* (or resolve to) a new response, or throw/reject. | ||
*/ | ||
/** | ||
* @summary Refresh token on user request | ||
* @function | ||
* @public | ||
* | ||
* @description | ||
* This function automatically refreshes the authentication token. | ||
* | ||
* @param {String} options.url - relative url | ||
* | ||
* @returns {String} token - new token | ||
* | ||
* @example | ||
* request.refreshToken | ||
* baseUrl: 'https://api.balena-cloud.com' | ||
*/ | ||
exports.refreshToken = function(options) { | ||
var baseUrl; | ||
if (options == null) { | ||
options = {}; | ||
} | ||
baseUrl = options.baseUrl; | ||
if (!(auth != null)) { | ||
throw new Error('Auth module not provided in initializer'); | ||
} | ||
return exports.send({ | ||
url: '/whoami', | ||
baseUrl: baseUrl, | ||
refreshToken: false | ||
})["catch"]({ | ||
code: 'BalenaRequestError', | ||
statusCode: 401 | ||
}, function() { | ||
return auth.getKey().tap(auth.removeKey).then(function(key) { | ||
throw new errors.BalenaExpiredToken(key); | ||
}); | ||
}).get('body').tap(auth.setKey); | ||
}; | ||
return exports; | ||
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) { | ||
const shouldRefreshKey = await utils.shouldRefreshKey(auth); | ||
if (shouldRefreshKey) { | ||
await exports.refreshToken(options); | ||
} | ||
} | ||
const authorizationHeader = options.sendToken | ||
? await utils.getAuthorizationHeader(auth) | ||
: undefined; | ||
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}`; | ||
} | ||
return options; | ||
}; | ||
const interceptRequestOptions = (requestOptions) => interceptRequestOrError(Promise.resolve(requestOptions)); | ||
const interceptRequestError = (requestError) => interceptRequestOrError(Promise.reject(requestError)); | ||
const interceptResponse = (response) => interceptResponseOrError(Promise.resolve(response)); | ||
const interceptResponseError = (responseError) => interceptResponseOrError(Promise.reject(responseError)); | ||
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 = async function (initialPromise) { | ||
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); | ||
}; | ||
/** | ||
* @summary Perform an HTTP request to balena | ||
* @function | ||
* @public | ||
* | ||
* @description | ||
* This function automatically handles authorization with balena. | ||
* | ||
* The module scans your environment for a saved session token. Alternatively, you may pass the `apiKey` option. Otherwise, the request is made anonymously. | ||
* | ||
* Requests can be aborted using an AbortController (with a polyfill like https://www.npmjs.com/package/abortcontroller-polyfill | ||
* if necessary). This is not well supported everywhere yet, is on a best-efforts basis, and should not be relied upon. | ||
* | ||
* @param {Object} options - options | ||
* @param {String} [options.method='GET'] - method | ||
* @param {String} options.url - relative url | ||
* @param {String} [options.apiKey] - api key | ||
* @param {String} [options.responseFormat] - explicit expected response format, | ||
* can be one of 'blob', 'json', 'text', 'none'. Defaults to sniffing the content-type | ||
* @param {AbortSignal} [options.signal] - a signal from an AbortController | ||
* @param {*} [options.body] - body | ||
* @param {number} [options.timeout] - body | ||
* | ||
* @returns {Promise<Object>} response | ||
* | ||
* @example | ||
* request.send | ||
* method: 'GET' | ||
* baseUrl: 'https://api.balena-cloud.com' | ||
* url: '/foo' | ||
* .get('body') | ||
* | ||
* @example | ||
* request.send | ||
* method: 'POST' | ||
* baseUrl: 'https://api.balena-cloud.com' | ||
* url: '/bar' | ||
* data: | ||
* hello: 'world' | ||
* .get('body') | ||
*/ | ||
exports.send = async 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(async (opts) => { | ||
let response; | ||
try { | ||
response = await requestAsync(opts); | ||
} | ||
catch (err) { | ||
err.requestOptions = opts; | ||
throw err; | ||
} | ||
const body = await utils.getBody(response, options.responseFormat); | ||
response = { ...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); | ||
}; | ||
/** | ||
* @summary Stream an HTTP response from balena. | ||
* @function | ||
* @public | ||
* | ||
* @description | ||
* This function emits a `progress` event, passing an object with the following properties: | ||
* | ||
* - `Number percent`: from 0 to 100. | ||
* - `Number total`: total bytes to be transmitted. | ||
* - `Number received`: number of bytes transmitted. | ||
* - `Number eta`: estimated remaining time, in seconds. | ||
* | ||
* The stream may also contain the following custom properties: | ||
* | ||
* - `String .mime`: Equals the value of the `Content-Type` HTTP header. | ||
* | ||
* See `request.send()` for an explanation on how this function handles authentication, and details | ||
* on how to abort requests. | ||
* | ||
* @param {Object} options - options | ||
* @param {String} [options.method='GET'] - method | ||
* @param {String} options.url - relative url | ||
* @param {*} [options.body] - body | ||
* | ||
* @returns {Promise<ReadableStream>} response | ||
* | ||
* @example | ||
* request.stream | ||
* method: 'GET' | ||
* baseUrl: 'https://img.balena-cloud.com' | ||
* url: '/download/foo' | ||
* .then (stream) -> | ||
* stream.on 'progress', (state) -> | ||
* console.log(state) | ||
* | ||
* stream.pipe(fs.createWriteStream('/opt/download')) | ||
*/ | ||
exports.stream = function (options) { | ||
const requestStream = isBrowser ? requestBrowserStream : requestAsync; | ||
return prepareOptions(options) | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then(async (opts) => { | ||
const download = await (progress.estimate(requestStream, isBrowser)(opts)); | ||
// @ts-expect-error | ||
if (!utils.isErrorCode(download.response.statusCode)) { | ||
// TODO: Move this to balena-image-manager | ||
// @ts-expect-error | ||
download.mime = download.response.headers.get('Content-Type'); | ||
return download; | ||
} | ||
// If status code is an error code, interpret | ||
// the body of the request as an error. | ||
const data = await rindle.extract(download); | ||
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); | ||
}) | ||
.then(interceptResponse, interceptResponseError); | ||
}; | ||
/** | ||
* @summary Array of interceptors | ||
* @type {Interceptor[]} | ||
* @public | ||
* | ||
* @description | ||
* The current array of interceptors to use. Interceptors intercept requests made | ||
* by calls to `.stream()` and `.send()` (some of which are made internally) and | ||
* are executed in the order they appear in this array for requests, and in the | ||
* reverse order for responses. | ||
* | ||
* @example | ||
* request.interceptors.push( | ||
* requestError: (error) -> | ||
* console.log(error) | ||
* throw error | ||
* ) | ||
*/ | ||
exports.interceptors = interceptors; | ||
/** | ||
* @typedef Interceptor | ||
* @type {object} | ||
* | ||
* @description | ||
* An interceptor implements some set of the four interception hook callbacks. | ||
* To continue processing, each function should return a value or a promise that | ||
* successfully resolves to a value. | ||
* | ||
* To halt processing, each function should throw an error or return a promise that | ||
* rejects with an error. | ||
* | ||
* @property {function} [request] - Callback invoked before requests are made. Called with | ||
* the request options, should return (or resolve to) new request options, or throw/reject. | ||
* | ||
* @property {function} [response] - Callback invoked before responses are returned. Called with | ||
* the response, should return (or resolve to) a new response, or throw/reject. | ||
* | ||
* @property {function} [requestError] - Callback invoked if an error happens before a request. | ||
* Called with the error itself, caused by a preceeding request interceptor rejecting/throwing | ||
* an error for the request, or a failing in preflight token validation. Should return (or resolve | ||
* to) new request options, or throw/reject. | ||
* | ||
* @property {function} [responseError] - Callback invoked if an error happens in the response. | ||
* Called with the error itself, caused by a preceeding response interceptor rejecting/throwing | ||
* an error for the request, a network error, or an error response from the server. Should return | ||
* (or resolve to) a new response, or throw/reject. | ||
*/ | ||
/** | ||
* @summary Refresh token on user request | ||
* @function | ||
* @public | ||
* | ||
* @description | ||
* This function automatically refreshes the authentication token. | ||
* | ||
* @param {object} options | ||
* @param {String} options.baseUrl - relative url | ||
* | ||
* @returns {Promise<String>} token - new token | ||
* | ||
* @example | ||
* request.refreshToken | ||
* baseUrl: 'https://api.balena-cloud.com' | ||
*/ | ||
exports.refreshToken = async function ({ baseUrl }) { | ||
// Only refresh if we have balena-auth | ||
if (auth == null) { | ||
throw new Error('Auth module not provided in initializer'); | ||
} | ||
let response; | ||
try { | ||
response = await exports.send({ | ||
url: '/whoami', | ||
baseUrl, | ||
refreshToken: false, | ||
}); | ||
} | ||
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; | ||
}; | ||
return exports; | ||
}; |
@@ -1,5 +0,4 @@ | ||
// Generated by CoffeeScript 1.12.7 | ||
"use strict"; | ||
/* | ||
Copyright 2016 Balena | ||
Copyright 2016-2020 Balena Ltd. | ||
@@ -10,3 +9,3 @@ Licensed under the Apache License, Version 2.0 (the "License"); | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
@@ -18,27 +17,15 @@ Unless required by applicable law or agreed to in writing, software | ||
limitations under the License. | ||
*/ | ||
var HeadersPonyfill, IS_BROWSER, Promise, UNSUPPORTED_REQUEST_PARAMS, errors, handleAbortIfNotSupported, normalFetch, processRequestOptions, qs, ref, requestAsync, urlLib; | ||
Promise = require('bluebird'); | ||
ref = require('fetch-ponyfill')({ | ||
Promise: Promise | ||
}), normalFetch = ref.fetch, HeadersPonyfill = ref.Headers; | ||
urlLib = require('url'); | ||
qs = require('qs'); | ||
errors = require('balena-errors'); | ||
IS_BROWSER = typeof window !== "undefined" && window !== null; | ||
*/ | ||
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 { fetch: normalFetch, Headers: HeadersPonyfill, } = require('fetch-ponyfill')({ Promise }); | ||
const urlLib = require("url"); | ||
const qs = require("qs"); | ||
const errors = require("balena-errors"); | ||
const token_1 = require("balena-auth/build/token"); | ||
const IS_BROWSER = typeof window !== 'undefined' && window !== null; | ||
/** | ||
* @module utils | ||
*/ | ||
exports.TOKEN_REFRESH_INTERVAL = 1 * 1000 * 60 * 60; | ||
exports.TOKEN_REFRESH_INTERVAL = 1 * 1000 * 60 * 60; // 1 hour in milliseconds | ||
/** | ||
@@ -53,3 +40,3 @@ * @summary Determine if the token should be updated | ||
* | ||
* @param {Object} tokenInstance - an instance of `balena-auth` | ||
* @param {import('balena-auth').default} auth - an instance of `balena-auth` | ||
* @returns {Promise<Boolean>} the token should be updated | ||
@@ -59,23 +46,19 @@ * | ||
* tokenUtils.shouldRefreshKey(tokenInstance).then (shouldRefreshKey) -> | ||
* if shouldRefreshKey | ||
* console.log('Updating token!') | ||
* if shouldRefreshKey | ||
* console.log('Updating token!') | ||
*/ | ||
exports.shouldRefreshKey = function(auth) { | ||
return auth.hasKey().then(function(hasKey) { | ||
async function shouldRefreshKey(auth) { | ||
const hasKey = await auth.hasKey(); | ||
if (!hasKey) { | ||
return false; | ||
return false; | ||
} | ||
return auth.getType().then(function(type) { | ||
if (type !== 'JWT') { | ||
const type = await auth.getType(); | ||
if (type !== token_1.TokenType.JWT) { | ||
return false; | ||
} | ||
return auth.getAge().then(function(age) { | ||
return age >= exports.TOKEN_REFRESH_INTERVAL; | ||
}); | ||
}); | ||
}); | ||
}; | ||
} | ||
const age = await auth.getAge(); | ||
// @ts-expect-error | ||
return age >= exports.TOKEN_REFRESH_INTERVAL; | ||
} | ||
exports.shouldRefreshKey = shouldRefreshKey; | ||
/** | ||
@@ -89,26 +72,21 @@ * @summary Get authorization header content | ||
* | ||
* @param {Object} tokenInstance - an instance of `balena-auth` | ||
* @returns {Promise<String>} authorization header | ||
* @param {import('balena-auth').default} auth - an instance of `balena-auth` | ||
* @returns {Promise<string | undefined>} authorization header | ||
* | ||
* @example | ||
* utils.getAuthorizationHeader(tokenInstance).then (authorizationHeader) -> | ||
* headers = | ||
* Authorization: authorizationHeader | ||
* headers = | ||
* Authorization: authorizationHeader | ||
*/ | ||
exports.getAuthorizationHeader = Promise.method(function(auth) { | ||
if (auth == null) { | ||
return; | ||
} | ||
return auth.hasKey().then(function(hasKey) { | ||
exports.getAuthorizationHeader = async function (auth) { | ||
if (auth == null) { | ||
return; | ||
} | ||
const hasKey = await auth.hasKey(); | ||
if (!hasKey) { | ||
return; | ||
return; | ||
} | ||
return auth.getKey().then(function(key) { | ||
return "Bearer " + key; | ||
}); | ||
}); | ||
}); | ||
const key = await auth.getKey(); | ||
return `Bearer ${key}`; | ||
}; | ||
/** | ||
@@ -124,22 +102,20 @@ * @summary Get error message from response | ||
* request | ||
* method: 'GET' | ||
* url: 'https://foo.bar' | ||
* , (error, response) -> | ||
* throw error if error? | ||
* message = utils.getErrorMessageFromResponse(response) | ||
* method: 'GET' | ||
* url: 'https://foo.bar' | ||
* , (error, response) -> | ||
* throw error if error? | ||
* message = utils.getErrorMessageFromResponse(response) | ||
*/ | ||
exports.getErrorMessageFromResponse = function(response) { | ||
var errorText, ref1; | ||
if (!response.body) { | ||
return 'The request was unsuccessful'; | ||
} | ||
errorText = (ref1 = response.body.error) != null ? ref1.text : void 0; | ||
if (errorText != null) { | ||
return errorText; | ||
} | ||
return response.body; | ||
}; | ||
function getErrorMessageFromResponse(response) { | ||
var _a; | ||
if (!response.body) { | ||
return 'The request was unsuccessful'; | ||
} | ||
const errorText = (_a = response.body.error) === null || _a === void 0 ? void 0 : _a.text; | ||
if (errorText != null) { | ||
return errorText; | ||
} | ||
return response.body; | ||
} | ||
exports.getErrorMessageFromResponse = getErrorMessageFromResponse; | ||
/** | ||
@@ -155,10 +131,8 @@ * @summary Check if the status code represents an error | ||
* if utils.isErrorCode(400) | ||
* console.log('400 is an error code!') | ||
* console.log('400 is an error code!') | ||
*/ | ||
exports.isErrorCode = function(statusCode) { | ||
return statusCode >= 400; | ||
}; | ||
function isErrorCode(statusCode) { | ||
return statusCode >= 400; | ||
} | ||
exports.isErrorCode = isErrorCode; | ||
/** | ||
@@ -176,8 +150,6 @@ * @summary Check whether a response body is compressed | ||
*/ | ||
exports.isResponseCompressed = function(response) { | ||
return response.headers.get('Content-Encoding') === 'gzip'; | ||
}; | ||
function isResponseCompressed(response) { | ||
return response.headers.get('Content-Encoding') === 'gzip'; | ||
} | ||
exports.isResponseCompressed = isResponseCompressed; | ||
/** | ||
@@ -196,11 +168,11 @@ * @summary Get response compressed/uncompressed length | ||
*/ | ||
exports.getResponseLength = function(response) { | ||
return { | ||
uncompressed: parseInt(response.headers.get('Content-Length'), 10) || void 0, | ||
compressed: parseInt(response.headers.get('X-Transfer-Length'), 10) || void 0 | ||
}; | ||
}; | ||
function getResponseLength(response) { | ||
return { | ||
uncompressed: parseInt(response.headers.get('Content-Length'), 10) || undefined, | ||
// X-Transfer-Length equals the compressed size of the body. | ||
// This header is sent by Image Maker when downloading OS images. | ||
compressed: parseInt(response.headers.get('X-Transfer-Length'), 10) || undefined, | ||
}; | ||
} | ||
exports.getResponseLength = getResponseLength; | ||
/** | ||
@@ -217,3 +189,3 @@ * @summary Print debug information about a request/response. | ||
* method: 'GET' | ||
* url: '/foo' | ||
* url: '/foo' | ||
* } | ||
@@ -224,61 +196,90 @@ * | ||
*/ | ||
exports.debugRequest = function(options, response) { | ||
return console.error(Object.assign({ | ||
statusCode: response.statusCode, | ||
duration: response.duration | ||
}, options)); | ||
}; | ||
UNSUPPORTED_REQUEST_PARAMS = ['qsParseOptions', 'qsStringifyOptions', 'useQuerystring', 'form', 'formData', 'multipart', 'preambleCRLF', 'postambleCRLF', 'jsonReviver', 'jsonReplacer', 'auth', 'oauth', 'aws', 'httpSignature', 'followAllRedirects', 'maxRedirects', 'removeRefererHeader', 'encoding', 'jar', 'agent', 'agentClass', 'agentOptions', 'forever', 'pool', 'localAddress', 'proxy', 'proxyHeaderWhiteList', 'proxyHeaderExclusiveList', 'time', 'har', 'callback']; | ||
processRequestOptions = function(options) { | ||
var body, headers, i, key, len, opts, params, url; | ||
if (options == null) { | ||
options = {}; | ||
} | ||
url = options.url || options.uri; | ||
if (options.baseUrl) { | ||
url = urlLib.resolve(options.baseUrl, url); | ||
} | ||
if (options.qs) { | ||
params = qs.stringify(options.qs); | ||
url += (url.indexOf('?') >= 0 ? '&' : '?') + params; | ||
} | ||
opts = {}; | ||
opts.timeout = options.timeout; | ||
opts.retries = options.retries; | ||
opts.method = options.method; | ||
opts.compress = options.gzip; | ||
opts.signal = options.signal; | ||
body = options.body, headers = options.headers; | ||
if (headers == null) { | ||
headers = {}; | ||
} | ||
if (options.json && body) { | ||
body = JSON.stringify(body); | ||
headers['Content-Type'] = 'application/json'; | ||
} | ||
opts.body = body; | ||
if (!IS_BROWSER) { | ||
headers['Accept-Encoding'] || (headers['Accept-Encoding'] = 'compress, gzip'); | ||
} | ||
if (options.followRedirect) { | ||
opts.redirect = 'follow'; | ||
} | ||
opts.headers = new HeadersPonyfill(headers); | ||
if (options.strictSSL === false) { | ||
throw new Error('`strictSSL` must be true or absent'); | ||
} | ||
for (i = 0, len = UNSUPPORTED_REQUEST_PARAMS.length; i < len; i++) { | ||
key = UNSUPPORTED_REQUEST_PARAMS[i]; | ||
if (options[key] != null) { | ||
throw new Error("The " + key + " param is not supported. Value: " + options[key]); | ||
function debugRequest(options, response) { | ||
return console.error(Object.assign({ | ||
statusCode: response.statusCode, | ||
duration: response.duration, | ||
}, options)); | ||
} | ||
exports.debugRequest = debugRequest; | ||
// fetch adapter | ||
const UNSUPPORTED_REQUEST_PARAMS = [ | ||
'qsParseOptions', | ||
'qsStringifyOptions', | ||
'useQuerystring', | ||
'form', | ||
'formData', | ||
'multipart', | ||
'preambleCRLF', | ||
'postambleCRLF', | ||
'jsonReviver', | ||
'jsonReplacer', | ||
'auth', | ||
'oauth', | ||
'aws', | ||
'httpSignature', | ||
'followAllRedirects', | ||
'maxRedirects', | ||
'removeRefererHeader', | ||
'encoding', | ||
'jar', | ||
'agent', | ||
'agentClass', | ||
'agentOptions', | ||
'forever', | ||
'pool', | ||
'localAddress', | ||
'proxy', | ||
'proxyHeaderWhiteList', | ||
'proxyHeaderExclusiveList', | ||
'time', | ||
'har', | ||
'callback', | ||
]; | ||
const processRequestOptions = function (options) { | ||
if (options == null) { | ||
options = {}; | ||
} | ||
} | ||
opts.mode = 'cors'; | ||
return [url, opts]; | ||
let url = options.url || options.uri; | ||
if (options.baseUrl) { | ||
url = urlLib.resolve(options.baseUrl, url); | ||
} | ||
if (options.qs) { | ||
const params = qs.stringify(options.qs); | ||
url += (url.indexOf('?') >= 0 ? '&' : '?') + params; | ||
} | ||
const opts = {}; | ||
opts.timeout = options.timeout; | ||
opts.retries = options.retries; | ||
opts.method = options.method; | ||
opts.compress = options.gzip; | ||
opts.signal = options.signal; | ||
let { body, headers } = options; | ||
if (headers == null) { | ||
headers = {}; | ||
} | ||
if (options.json && body) { | ||
body = JSON.stringify(body); | ||
headers['Content-Type'] = 'application/json'; | ||
} | ||
opts.body = body; | ||
if (!IS_BROWSER) { | ||
if (!headers['Accept-Encoding']) { | ||
headers['Accept-Encoding'] = 'compress, gzip'; | ||
} | ||
} | ||
if (options.followRedirect) { | ||
opts.redirect = 'follow'; | ||
} | ||
opts.headers = new HeadersPonyfill(headers); | ||
if (options.strictSSL === false) { | ||
throw new Error('`strictSSL` must be true or absent'); | ||
} | ||
for (let key of UNSUPPORTED_REQUEST_PARAMS) { | ||
if (options[key] != null) { | ||
throw new Error(`The ${key} param is not supported. Value: ${options[key]}`); | ||
} | ||
} | ||
opts.mode = 'cors'; | ||
return [url, opts]; | ||
}; | ||
/** | ||
@@ -297,91 +298,107 @@ * @summary Extract the body from the server response | ||
*/ | ||
exports.getBody = function(response, responseFormat) { | ||
return Promise["try"](function() { | ||
var contentType; | ||
async function getBody(response, responseFormat) { | ||
if (responseFormat === 'none') { | ||
return null; | ||
return null; | ||
} | ||
contentType = response.headers.get('Content-Type'); | ||
if (responseFormat === 'blob' || ((responseFormat == null) && (contentType != null ? contentType.includes('binary/octet-stream') : void 0))) { | ||
if (typeof response.blob === 'function') { | ||
return response.blob(); | ||
} | ||
if (typeof response.buffer === 'function') { | ||
return response.buffer(); | ||
} | ||
throw new Error('This `fetch` implementation does not support decoding binary streams.'); | ||
const contentType = response.headers.get('Content-Type'); | ||
if (responseFormat === 'blob' || | ||
(responseFormat == null && (contentType === null || contentType === void 0 ? void 0 : contentType.includes('binary/octet-stream')))) { | ||
// 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-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 === 'json' || ((responseFormat == null) && (contentType != null ? contentType.includes('application/json') : void 0))) { | ||
return response.json(); | ||
if (responseFormat === 'json' || | ||
(responseFormat == null && (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/json')))) { | ||
return response.json(); | ||
} | ||
if ((responseFormat == null) || responseFormat === 'text') { | ||
return response.text(); | ||
if (responseFormat == null || responseFormat === 'text') { | ||
return response.text(); | ||
} | ||
throw new errors.BalenaInvalidParameterError('responseFormat', responseFormat); | ||
}); | ||
}; | ||
requestAsync = function(fetch, options, retriesRemaining) { | ||
var nativeHeaders, opts, p, ref1, requestTime, url; | ||
ref1 = processRequestOptions(options), url = ref1[0], opts = ref1[1]; | ||
if (retriesRemaining == null) { | ||
retriesRemaining = opts.retries; | ||
} | ||
if (fetch !== normalFetch && typeof Headers === 'function') { | ||
nativeHeaders = new Headers(); | ||
opts.headers.forEach(function(value, name) { | ||
return nativeHeaders.append(name, value); | ||
}); | ||
opts.headers = nativeHeaders; | ||
} | ||
requestTime = new Date(); | ||
p = fetch(url, opts); | ||
if (opts.timeout && IS_BROWSER) { | ||
p = p.timeout(opts.timeout); | ||
} | ||
p = p.then(function(response) { | ||
var responseTime; | ||
if (opts.signal) { | ||
handleAbortIfNotSupported(opts.signal, response); | ||
} | ||
exports.getBody = getBody; | ||
// This is the actual implementation that hides the internal `retriesRemaining` parameter | ||
var requestAsync = async function (fetch, options, retriesRemaining) { | ||
const [url, opts] = processRequestOptions(options); | ||
if (retriesRemaining == null) { | ||
retriesRemaining = opts.retries; | ||
} | ||
responseTime = new Date(); | ||
response.duration = responseTime - requestTime; | ||
response.statusCode = response.status; | ||
response.request = { | ||
headers: options.headers, | ||
uri: urlLib.parse(url) | ||
}; | ||
return response; | ||
}); | ||
if (retriesRemaining > 0) { | ||
return p["catch"](function() { | ||
return requestAsync(fetch, options, retriesRemaining - 1); | ||
}); | ||
} else { | ||
return p; | ||
} | ||
// 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 = await 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 await requestAsync(fetch, options, retriesRemaining - 1); | ||
} | ||
throw err; | ||
} | ||
}; | ||
handleAbortIfNotSupported = function(signal, response) { | ||
var emulateAbort, ref1, ref2; | ||
emulateAbort = ((ref1 = response.body) != null ? ref1.cancel : void 0) ? function() { | ||
return response.body.cancel()["catch"](function() {}); | ||
} : ((ref2 = response.body) != null ? ref2.destroy : void 0) ? function() { | ||
return response.body.destroy(); | ||
} : void 0; | ||
if (emulateAbort) { | ||
if (signal.aborted) { | ||
return emulateAbort(); | ||
} else { | ||
return signal.addEventListener('abort', function() { | ||
return emulateAbort(); | ||
}, { | ||
once: true | ||
}); | ||
var handleAbortIfNotSupported = function (signal, response) { | ||
const emulateAbort = (() => { | ||
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 | ||
// Context: https://github.com/jonnyreeves/fetch-readablestream/issues/6 | ||
return () => response.body.cancel().catch(function () { | ||
// ignore | ||
}); | ||
} | ||
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 | ||
// the underlying connection (hard to confirm - but it definitely stops streaming) | ||
// Once https://github.com/bitinn/node-fetch/issues/95 is released, we should | ||
// use that instead. | ||
return () => response.body.destroy(); | ||
} | ||
})(); | ||
if (emulateAbort) { | ||
if (signal.aborted) { | ||
return emulateAbort(); | ||
} | ||
else { | ||
return signal.addEventListener('abort', () => emulateAbort(), { | ||
once: true, | ||
}); | ||
} | ||
} | ||
} | ||
}; | ||
/** | ||
@@ -402,10 +419,8 @@ * @summary The factory that returns the `requestAsync` function. | ||
*/ | ||
exports.getRequestAsync = function(fetch) { | ||
if (fetch == null) { | ||
fetch = normalFetch; | ||
} | ||
return function(options) { | ||
return requestAsync(fetch, options); | ||
}; | ||
}; | ||
function getRequestAsync(fetch) { | ||
if (fetch == null) { | ||
fetch = normalFetch; | ||
} | ||
return (options) => requestAsync(fetch, options); | ||
} | ||
exports.getRequestAsync = getRequestAsync; |
@@ -7,2 +7,9 @@ # Change Log | ||
# v11.0.0 | ||
## (2020-07-02) | ||
* Drop support for nodejs < 10 [Pagan Gazzard] | ||
* Switch to returning native promises [Pagan Gazzard] | ||
* Convert to type checked javascript [Pagan Gazzard] | ||
# v10.0.9 | ||
@@ -9,0 +16,0 @@ ## (2020-04-20) |
{ | ||
"name": "balena-request", | ||
"version": "10.0.9", | ||
"version": "11.0.0-11-x-0b9fa20803d50fba3fa44bb8c914db76f9dfaaf3", | ||
"description": "Balena HTTP client", | ||
@@ -20,10 +20,13 @@ "main": "build/request.js", | ||
"scripts": { | ||
"lint": "gulp lint", | ||
"lint": "balena-lint -e js --typescript lib", | ||
"lint-fix": "balena-lint -e js --typescript --fix lib", | ||
"pretest": "npm run build", | ||
"test": "npm run test-node && npm run test-browser", | ||
"test-node": "gulp test", | ||
"posttest": "npm run lint", | ||
"test-node": "mocha -r coffeescript/register tests/**/*.spec.coffee", | ||
"test-browser": "mockttp -c karma start", | ||
"build": "gulp build", | ||
"build": "npx tsc", | ||
"prepublish": "require-npm4-to-publish", | ||
"prepare": "npm run build", | ||
"readme": "jsdoc2md --template doc/README.hbs build/request.js build/progress.js build/utils.js > README.md" | ||
"readme": "jsdoc2md --template doc/README.hbs lib/request.js lib/progress.js lib/utils.js > README.md" | ||
}, | ||
@@ -33,11 +36,8 @@ "author": "Juan Cruz Viotti <juanchiviotti@gmail.com>", | ||
"devDependencies": { | ||
"balena-auth": "^3.0.1", | ||
"coffee-script": "~1.12.7", | ||
"@balena/lint": "^5.1.0", | ||
"balena-auth": "^3.1.1", | ||
"bluebird": "^3.7.2", | ||
"coffeescript": "~1.12.7", | ||
"global-tunnel-ng": "2.1.0", | ||
"gulp": "^4.0.2", | ||
"gulp-coffee": "^2.3.5", | ||
"gulp-coffeelint": "^0.6.0", | ||
"gulp-mocha": "^4.3.1", | ||
"gulp-util": "^3.0.8", | ||
"jsdoc-to-markdown": "^4.0.1", | ||
"jsdoc-to-markdown": "^6.0.1", | ||
"karma": "^1.7.1", | ||
@@ -53,2 +53,3 @@ "karma-chrome-launcher": "^2.2.0", | ||
"timekeeper": "^1.0.0", | ||
"typescript": "^3.9.6", | ||
"zlib-browserify": "0.0.3" | ||
@@ -58,9 +59,8 @@ }, | ||
"@balena/node-web-streams": "^0.2.3", | ||
"balena-errors": "^4.2.1", | ||
"bluebird": "^3.7.2", | ||
"balena-errors": "^4.4.0", | ||
"fetch-ponyfill": "^4.1.0", | ||
"fetch-readablestream": "^0.2.0", | ||
"progress-stream": "^2.0.0", | ||
"qs": "^6.9.1", | ||
"rindle": "^1.3.4" | ||
"qs": "^6.9.4", | ||
"rindle": "^1.3.6" | ||
}, | ||
@@ -67,0 +67,0 @@ "peerDependencies": { |
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
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
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
143777
8
18
1817
1
11
- Removedbluebird@^3.7.2
Updatedbalena-errors@^4.4.0
Updatedqs@^6.9.4
Updatedrindle@^1.3.6