balena-request
Advanced tools
Comparing version 13.3.4 to 14.0.0-build-14-x-87bd6d494fc65c3fbcef83655bc65651c671782f-1
@@ -20,3 +20,3 @@ "use strict"; | ||
const tslib_1 = require("tslib"); | ||
const utils = require("./utils"); | ||
const utils = tslib_1.__importStar(require("./utils")); | ||
/** | ||
@@ -84,81 +84,79 @@ * @module progress | ||
function estimate(requestAsync, isBrowser) { | ||
return function (options) { | ||
return tslib_1.__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); | ||
return async 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 }); | ||
} | ||
const response = await requestAsync(options); | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const stream = require('stream'); | ||
const output = new stream.PassThrough(); | ||
output.response = response; | ||
const responseLength = utils.getResponseLength(response); | ||
const total = responseLength.uncompressed || responseLength.compressed; | ||
let responseStream; | ||
if (response.body.getReader) { | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const stream = require('stream'); | ||
const output = new stream.PassThrough(); | ||
output.response = response; | ||
const responseLength = utils.getResponseLength(response); | ||
const total = responseLength.uncompressed || responseLength.compressed; | ||
let responseStream; | ||
if (response.body.getReader) { | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const webStreams = require('@balena/node-web-streams'); | ||
// Convert browser (WHATWG) streams to Node streams | ||
responseStream = webStreams.toNodeReadable(response.body); | ||
reader = responseStream._reader; | ||
const webStreams = require('@balena/node-web-streams'); | ||
// 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)) { | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const zlib = require('zlib'); | ||
// Be more lenient with decoding compressed responses, since (very rarely) | ||
// servers send slightly invalid gzip responses that are still accepted | ||
// by common browsers. | ||
// Always using Z_SYNC_FLUSH is what cURL does. | ||
let zlibOptions = { | ||
flush: zlib.constants.Z_SYNC_FLUSH, | ||
finishFlush: zlib.constants.Z_SYNC_FLUSH, | ||
}; | ||
// Allow overriding this behaviour by setting the ZLIB_FLUSH env var | ||
// to one of the allowed zlib flush values (process.env.ZLIB_FLUSH="Z_NO_FLUSH"). | ||
// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants | ||
if (process.env.ZLIB_FLUSH && process.env.ZLIB_FLUSH in zlib.constants) { | ||
zlibOptions = { | ||
flush: zlib.constants[process.env.ZLIB_FLUSH], | ||
finishFlush: zlib.constants[process.env.ZLIB_FLUSH], | ||
}; | ||
} | ||
else { | ||
responseStream = response.body; | ||
const gunzip = zlib.createGunzip(zlibOptions); | ||
gunzip.on('error', (e) => output.emit('error', e)); | ||
// 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); | ||
} | ||
const progressStream = getProgressStream(total, (state) => output.emit('progress', state)); | ||
if (!isBrowser && utils.isResponseCompressed(response)) { | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const zlib = require('zlib'); | ||
// Be more lenient with decoding compressed responses, since (very rarely) | ||
// servers send slightly invalid gzip responses that are still accepted | ||
// by common browsers. | ||
// Always using Z_SYNC_FLUSH is what cURL does. | ||
let zlibOptions = { | ||
flush: zlib.constants.Z_SYNC_FLUSH, | ||
finishFlush: zlib.constants.Z_SYNC_FLUSH, | ||
}; | ||
// Allow overriding this behaviour by setting the ZLIB_FLUSH env var | ||
// to one of the allowed zlib flush values (process.env.ZLIB_FLUSH="Z_NO_FLUSH"). | ||
// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants | ||
if (process.env.ZLIB_FLUSH && process.env.ZLIB_FLUSH in zlib.constants) { | ||
zlibOptions = { | ||
flush: zlib.constants[process.env.ZLIB_FLUSH], | ||
finishFlush: zlib.constants[process.env.ZLIB_FLUSH], | ||
}; | ||
} | ||
const gunzip = zlib.createGunzip(zlibOptions); | ||
gunzip.on('error', (e) => output.emit('error', e)); | ||
// 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); | ||
responseStream.pipe(gunzip).pipe(progressStream).pipe(output); | ||
} | ||
// Stream any request errors on downstream | ||
responseStream.on('error', (e) => output.emit('error', e)); | ||
return output; | ||
}); | ||
} | ||
else { | ||
responseStream.pipe(progressStream).pipe(output); | ||
} | ||
// Stream any request errors on downstream | ||
responseStream.on('error', (e) => output.emit('error', e)); | ||
return output; | ||
}; | ||
} |
@@ -20,5 +20,5 @@ "use strict"; | ||
const tslib_1 = require("tslib"); | ||
const urlLib = require("url"); | ||
const errors = require("balena-errors"); | ||
const utils = require("./utils"); | ||
const urlLib = tslib_1.__importStar(require("url")); | ||
const errors = tslib_1.__importStar(require("balena-errors")); | ||
const utils = tslib_1.__importStar(require("./utils")); | ||
/** | ||
@@ -48,48 +48,46 @@ * @module request | ||
: utils.debugRequest; | ||
const prepareOptions = function (options) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
if (options == null) { | ||
options = {}; | ||
const prepareOptions = async function (options) { | ||
if (options == null) { | ||
options = {}; | ||
} | ||
const { baseUrl } = 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; | ||
} | ||
const isAbsoluteUrl = urlLib.parse(options.url).protocol != null; | ||
if (isAbsoluteUrl) { | ||
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 && | ||
(await utils.shouldRefreshKey(auth))) { | ||
if (baseUrl && !isAbsoluteUrl) { | ||
await refreshToken({ baseUrl }); | ||
} | ||
const { baseUrl } = 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 (await auth.isExpired()) { | ||
throw new errors.BalenaExpiredToken(await auth.getKey()); | ||
} | ||
const isAbsoluteUrl = urlLib.parse(options.url).protocol != null; | ||
if (isAbsoluteUrl) { | ||
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 && | ||
(yield utils.shouldRefreshKey(auth))) { | ||
if (baseUrl && !isAbsoluteUrl) { | ||
yield refreshToken({ baseUrl }); | ||
} | ||
if (yield auth.isExpired()) { | ||
throw new errors.BalenaExpiredToken(yield auth.getKey()); | ||
} | ||
} | ||
const authorizationHeader = options.sendToken | ||
? yield 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 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; | ||
}; | ||
@@ -100,3 +98,3 @@ const interceptRequestOptions = (requestOptions) => interceptRequestOrError(Promise.resolve(requestOptions)); | ||
const interceptResponseError = (responseError) => interceptResponseOrError(Promise.reject(responseError)); | ||
const interceptRequestOrError = (initialPromise) => tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const interceptRequestOrError = async (initialPromise) => { | ||
let promise = initialPromise; | ||
@@ -109,15 +107,13 @@ for (const { request, requestError } of exports.interceptors) { | ||
return promise; | ||
}); | ||
const interceptResponseOrError = function (initialPromise) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
let promise = initialPromise; | ||
for (const { response, responseError } of exports.interceptors | ||
.slice() | ||
.reverse()) { | ||
if (response != null || responseError != null) { | ||
promise = promise.then(response, responseError); | ||
} | ||
}; | ||
const interceptResponseOrError = async function (initialPromise) { | ||
let promise = initialPromise; | ||
for (const { response, responseError } of exports.interceptors | ||
.slice() | ||
.reverse()) { | ||
if (response != null || responseError != null) { | ||
promise = promise.then(response, responseError); | ||
} | ||
return promise; | ||
}); | ||
} | ||
return promise; | ||
}; | ||
@@ -165,38 +161,36 @@ /** | ||
*/ | ||
function send(options) { | ||
return tslib_1.__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; | ||
async function send(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); | ||
} | ||
return prepareOptions(options) | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then((opts) => tslib_1.__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); | ||
// We have to use Object.defineProperty in order to overwrite the body property | ||
// for node-fetch without losing all the other properties/methods | ||
Object.defineProperty(response, 'body', { | ||
get() { | ||
return body; | ||
}, | ||
}); | ||
if (utils.isErrorCode(response.statusCode)) { | ||
const responseError = utils.getErrorMessageFromResponse(response); | ||
debugRequest(options, response); | ||
throw new errors.BalenaRequestError(responseError, response.statusCode, options, response.headers); | ||
} | ||
return response; | ||
})) | ||
.then(interceptResponse, interceptResponseError); | ||
}); | ||
catch (err) { | ||
err.requestOptions = opts; | ||
throw err; | ||
} | ||
const body = await utils.getBody(response, options.responseFormat); | ||
// We have to use Object.defineProperty in order to overwrite the body property | ||
// for node-fetch without losing all the other properties/methods | ||
Object.defineProperty(response, 'body', { | ||
get() { | ||
return body; | ||
}, | ||
}); | ||
if (utils.isErrorCode(response.statusCode)) { | ||
const responseError = utils.getErrorMessageFromResponse(response); | ||
debugRequest(options, response); | ||
throw new errors.BalenaRequestError(responseError, response.statusCode, options, response.headers); | ||
} | ||
return response; | ||
}) | ||
.then(interceptResponse, interceptResponseError); | ||
} | ||
@@ -246,4 +240,4 @@ /** | ||
.then(interceptRequestOptions, interceptRequestError) | ||
.then((opts) => tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const download = yield progress.estimate(requestStream, isBrowser)(opts); | ||
.then(async (opts) => { | ||
const download = await progress.estimate(requestStream, isBrowser)(opts); | ||
if (!utils.isErrorCode(download.response.statusCode)) { | ||
@@ -255,3 +249,3 @@ // TODO: Move this to balena-image-manager | ||
// If status code is an error code, interpret the body of the request as an error. | ||
const data = yield utils.getStreamContents(download); | ||
const data = await utils.getStreamContents(download); | ||
const responseError = data || 'The request was unsuccessful'; | ||
@@ -261,3 +255,3 @@ debugRequest(options, download.response); | ||
throw new errors.BalenaRequestError(responseError, download.response.statusCode); | ||
})) | ||
}) | ||
.then((x) => interceptResponse(x), interceptResponseError); | ||
@@ -330,30 +324,28 @@ } | ||
*/ | ||
function refreshToken(_a) { | ||
return tslib_1.__awaiter(this, arguments, void 0, function* ({ baseUrl, }) { | ||
// Only refresh if we have balena-auth | ||
if (auth == null) { | ||
throw new Error('Auth module not provided in initializer'); | ||
async function refreshToken({ 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 send({ | ||
url: '/user/v1/refresh-token', | ||
baseUrl, | ||
refreshToken: false, | ||
}); | ||
} | ||
catch (err) { | ||
if (err.code === 'BalenaRequestError' && | ||
err.statusCode === 401 && | ||
(await auth.isExpired())) { | ||
const expiredKey = await auth.getKey(); | ||
await auth.removeKey(); | ||
throw new errors.BalenaExpiredToken(expiredKey); | ||
} | ||
let response; | ||
try { | ||
response = yield send({ | ||
url: '/user/v1/refresh-token', | ||
baseUrl, | ||
refreshToken: false, | ||
}); | ||
} | ||
catch (err) { | ||
if (err.code === 'BalenaRequestError' && | ||
err.statusCode === 401 && | ||
(yield auth.isExpired())) { | ||
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; | ||
}); | ||
throw err; | ||
} | ||
const refreshedKey = response.body; | ||
await auth.setKey(refreshedKey); | ||
return refreshedKey; | ||
} | ||
@@ -360,0 +352,0 @@ const exports = { |
@@ -33,5 +33,5 @@ "use strict"; | ||
require('fetch-ponyfill')({ Promise }); | ||
const urlLib = require("url"); | ||
const qs = require("qs"); | ||
const errors = require("balena-errors"); | ||
const urlLib = tslib_1.__importStar(require("url")); | ||
const qs = tslib_1.__importStar(require("qs")); | ||
const errors = tslib_1.__importStar(require("balena-errors")); | ||
const balena_auth_1 = require("balena-auth"); | ||
@@ -61,16 +61,14 @@ const stream_1 = require("stream"); | ||
*/ | ||
function shouldRefreshKey(auth) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
var _a; | ||
const hasKey = yield auth.hasKey(); | ||
if (!hasKey) { | ||
return false; | ||
} | ||
const type = yield auth.getType(); | ||
if (type !== balena_auth_1.TokenType.JWT) { | ||
return false; | ||
} | ||
const age = (_a = (yield auth.getAge())) !== null && _a !== void 0 ? _a : 0; | ||
return age >= exports.TOKEN_REFRESH_INTERVAL; | ||
}); | ||
async function shouldRefreshKey(auth) { | ||
var _a; | ||
const hasKey = await auth.hasKey(); | ||
if (!hasKey) { | ||
return false; | ||
} | ||
const type = await auth.getType(); | ||
if (type !== balena_auth_1.TokenType.JWT) { | ||
return false; | ||
} | ||
const age = (_a = (await auth.getAge())) !== null && _a !== void 0 ? _a : 0; | ||
return age >= exports.TOKEN_REFRESH_INTERVAL; | ||
} | ||
@@ -93,14 +91,12 @@ /** | ||
*/ | ||
function getAuthorizationHeader(auth) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
if (auth == null) { | ||
return; | ||
} | ||
const hasKey = yield auth.hasKey(); | ||
if (!hasKey) { | ||
return; | ||
} | ||
const key = yield auth.getKey(); | ||
return `Bearer ${key}`; | ||
}); | ||
async function getAuthorizationHeader(auth) { | ||
if (auth == null) { | ||
return; | ||
} | ||
const hasKey = await auth.hasKey(); | ||
if (!hasKey) { | ||
return; | ||
} | ||
const key = await auth.getKey(); | ||
return `Bearer ${key}`; | ||
} | ||
@@ -305,31 +301,29 @@ /** | ||
*/ | ||
function getBody(response, responseFormat) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
if (responseFormat === 'none') { | ||
return null; | ||
async function getBody(response, responseFormat) { | ||
if (responseFormat === 'none') { | ||
return null; | ||
} | ||
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(); | ||
} | ||
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 response happens to have a buffer function | ||
if (typeof response.buffer === 'function') { | ||
// @ts-expect-error calls it | ||
return response.buffer(); | ||
} | ||
throw new Error('This `fetch` implementation does not support decoding binary streams.'); | ||
// https://github.com/bitinn/node-fetch/blob/master/lib/body.js#L66 | ||
// @ts-expect-error if response happens to have a buffer function | ||
if (typeof response.buffer === 'function') { | ||
// @ts-expect-error calls it | ||
return response.buffer(); | ||
} | ||
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(); | ||
} | ||
throw new errors.BalenaInvalidParameterError('responseFormat', responseFormat); | ||
}); | ||
throw new Error('This `fetch` implementation does not support decoding binary streams.'); | ||
} | ||
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(); | ||
} | ||
throw new errors.BalenaInvalidParameterError('responseFormat', responseFormat); | ||
} | ||
@@ -351,88 +345,86 @@ const isFile = (value) => { | ||
// This is the actual implementation that hides the internal `retriesRemaining` parameter | ||
function requestAsync(fetch, options, retriesRemaining) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
var _a; | ||
const [url, opts] = processRequestOptions(options); | ||
if (retriesRemaining == null) { | ||
retriesRemaining = opts.retries; | ||
async function requestAsync(fetch, options, retriesRemaining) { | ||
var _a; | ||
const [url, opts] = 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 bodyEntries = Object.entries((_a = options.body) !== null && _a !== void 0 ? _a : {}); | ||
const fileKeys = new Set(); | ||
for (const [k, v] of bodyEntries) { | ||
if (isFile(v)) { | ||
fileKeys.add(k); | ||
} | ||
// 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 bodyEntries = Object.entries((_a = options.body) !== null && _a !== void 0 ? _a : {}); | ||
const fileKeys = new Set(); | ||
} | ||
if (fileKeys.size > 0) { | ||
const form = getForm(); | ||
for (const [k, v] of bodyEntries) { | ||
if (isFile(v)) { | ||
fileKeys.add(k); | ||
if (fileKeys.has(k)) { | ||
const file = v; | ||
form.append(k, file, file.name); | ||
} | ||
} | ||
if (fileKeys.size > 0) { | ||
const form = getForm(); | ||
for (const [k, v] of bodyEntries) { | ||
if (fileKeys.has(k)) { | ||
const file = v; | ||
form.append(k, file, file.name); | ||
} | ||
else { | ||
form.append(k, v); | ||
} | ||
} | ||
if (IS_BROWSER) { | ||
// Browsers will handle set form data header and boundaries | ||
// Given the correct body format | ||
opts.headers.delete('Content-Type'); | ||
opts.body = form; | ||
} | ||
else { | ||
const { FormDataEncoder } = | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
require('form-data-encoder'); | ||
const encoder = new FormDataEncoder(form); | ||
opts.headers.set('Content-Type', encoder.headers['Content-Type']); | ||
const length = encoder.headers['Content-Length']; | ||
if (length != null) { | ||
opts.headers.set('Content-Length', length); | ||
} | ||
// @ts-expect-error https://www.npmjs.com/package/form-data-encoder#usage | ||
opts.body = stream_1.Readable.from(encoder); | ||
form.append(k, v); | ||
} | ||
} | ||
try { | ||
const requestTime = Date.now(); | ||
let p = fetch(url, opts); | ||
if (opts.timeout && IS_BROWSER) { | ||
p = new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
reject(new Error('network timeout')); | ||
}, 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; | ||
if (IS_BROWSER) { | ||
// Browsers will handle set form data header and boundaries | ||
// Given the correct body format | ||
opts.headers.delete('Content-Type'); | ||
opts.body = form; | ||
} | ||
catch (err) { | ||
if (retriesRemaining > 0) { | ||
return yield requestAsync(fetch, options, retriesRemaining - 1); | ||
else { | ||
const { FormDataEncoder } = | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
require('form-data-encoder'); | ||
const encoder = new FormDataEncoder(form); | ||
opts.headers.set('Content-Type', encoder.headers['Content-Type']); | ||
const length = encoder.headers['Content-Length']; | ||
if (length != null) { | ||
opts.headers.set('Content-Length', length); | ||
} | ||
throw err; | ||
// @ts-expect-error https://www.npmjs.com/package/form-data-encoder#usage | ||
opts.body = stream_1.Readable.from(encoder); | ||
} | ||
}); | ||
} | ||
try { | ||
const requestTime = Date.now(); | ||
let p = fetch(url, opts); | ||
if (opts.timeout && IS_BROWSER) { | ||
p = new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
reject(new Error('network timeout')); | ||
}, 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; | ||
} | ||
} | ||
@@ -493,16 +485,14 @@ function handleAbortIfNotSupported(signal, response) { | ||
*/ | ||
function getStreamContents(stream) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const chunks = []; | ||
stream.on('data', function (chunk) { | ||
chunks.push(chunk); | ||
}); | ||
yield new Promise((resolve, reject) => { | ||
stream.on('error', reject); | ||
stream.on('close', resolve); | ||
stream.on('end', resolve); | ||
stream.on('done', resolve); | ||
}); | ||
return chunks.join(); | ||
async function getStreamContents(stream) { | ||
const chunks = []; | ||
stream.on('data', function (chunk) { | ||
chunks.push(chunk); | ||
}); | ||
await new Promise((resolve, reject) => { | ||
stream.on('error', reject); | ||
stream.on('close', resolve); | ||
stream.on('end', resolve); | ||
stream.on('done', resolve); | ||
}); | ||
return chunks.join(); | ||
} |
@@ -7,2 +7,9 @@ # Change Log | ||
## 14.0.0 - 2024-12-04 | ||
* Switch tsconfig module to Node16 [Pagan Gazzard] | ||
* Update form-data-encoder to 4.x and formdata-node to 6.x [Pagan Gazzard] | ||
* Update tsconfig target to es2017 [Pagan Gazzard] | ||
* Set `type` to `commonjs` in package.json [Pagan Gazzard] | ||
## 13.3.4 - 2024-12-04 | ||
@@ -9,0 +16,0 @@ |
{ | ||
"name": "balena-request", | ||
"version": "13.3.4", | ||
"version": "14.0.0-build-14-x-87bd6d494fc65c3fbcef83655bc65651c671782f-1", | ||
"description": "Balena HTTP client", | ||
"type": "commonjs", | ||
"main": "build/request.js", | ||
@@ -74,4 +75,4 @@ "types": "build/request.d.ts", | ||
"fetch-readablestream": "^0.2.0", | ||
"form-data-encoder": "1.7.2", | ||
"formdata-node": "^4.0.0", | ||
"form-data-encoder": "^4.0.2", | ||
"formdata-node": "^6.0.3", | ||
"progress-stream": "^2.0.0", | ||
@@ -86,4 +87,4 @@ "qs": "^6.9.4", | ||
"versionist": { | ||
"publishedAt": "2024-12-04T16:02:06.338Z" | ||
"publishedAt": "2024-12-04T16:35:59.225Z" | ||
} | ||
} |
import { expect } from 'chai'; | ||
import setup from './setup'; | ||
import * as stringToStream from 'string-to-stream'; | ||
import stringToStream from 'string-to-stream'; | ||
import * as sinon from 'sinon'; | ||
@@ -5,0 +5,0 @@ import * as mockhttp from 'mockttp'; |
@@ -22,3 +22,3 @@ const IS_BROWSER = typeof window !== 'undefined' && window !== null; | ||
import * as chai from 'chai'; | ||
import * as chaiAsPromised from 'chai-as-promised'; | ||
import chaiAsPromised from 'chai-as-promised'; | ||
chai.use(chaiAsPromised); | ||
@@ -25,0 +25,0 @@ |
@@ -1,3 +0,1 @@ | ||
import './browserify-zlib'; | ||
import { PassThrough } from 'stream'; | ||
@@ -4,0 +2,0 @@ import { expect } from 'chai'; |
import { expect } from 'chai'; | ||
import { TokenType } from 'balena-auth/build/token'; | ||
import setup from './setup'; | ||
import * as fetchPonyfill from 'fetch-ponyfill'; | ||
import fetchPonyfill from 'fetch-ponyfill'; | ||
import * as sinon from 'sinon'; | ||
@@ -6,0 +6,0 @@ import * as tokens from './tokens.json'; |
{ | ||
"compilerOptions": { | ||
"module": "commonjs", | ||
"module": "Node16", | ||
"declaration": true, | ||
@@ -10,6 +10,5 @@ "outDir": "build", | ||
"sourceMap": false, | ||
"target": "es2015", | ||
"target": "es2017", | ||
"noUnusedParameters": true, | ||
"noUnusedLocals": true, | ||
"moduleResolution": "node", | ||
"skipLibCheck": true, | ||
@@ -16,0 +15,0 @@ "importHelpers": true |
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
232686
4499
1
+ Addedform-data-encoder@4.0.2(transitive)
+ Addedformdata-node@6.0.3(transitive)
- Removedform-data-encoder@1.7.2(transitive)
- Removedformdata-node@4.4.1(transitive)
- Removednode-domexception@1.0.0(transitive)
- Removedweb-streams-polyfill@4.0.0-beta.3(transitive)
Updatedform-data-encoder@^4.0.2
Updatedformdata-node@^6.0.3