diff --git a/lib/progress.ts b/lib/progress.ts index a1131a2..78333f1 100644 --- a/lib/progress.ts +++ b/lib/progress.ts @@ -94,17 +94,10 @@ export interface BalenaRequestPassThroughStream extends Stream.PassThrough { * stream.on 'progress', (state) -> * console.log(state) */ -export function estimate( - requestAsync?: ReturnType, - isBrowser?: boolean, -) { +export function estimate(requestAsync: ReturnType) { return async function ( options: BalenaRequestOptions, ): Promise { - if (requestAsync == null) { - requestAsync = utils.getRequestAsync(); - } - options.gzip = false; options.headers!['Accept-Encoding'] = 'gzip, deflate'; @@ -155,51 +148,7 @@ export function estimate( output.emit('progress', state), ); - if (!isBrowser && utils.isResponseCompressed(response)) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const zlib = require('zlib') as typeof import('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 as keyof typeof zlib.constants - ], - finishFlush: - zlib.constants[ - process.env.ZLIB_FLUSH as keyof typeof zlib.constants - ], - }; - } - - 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(progressStream).pipe(output); // Stream any request errors on downstream responseStream.on('error', (e: Error) => output.emit('error', e)); diff --git a/lib/request.ts b/lib/request.ts index b1659b7..5680230 100644 --- a/lib/request.ts +++ b/lib/request.ts @@ -351,10 +351,7 @@ export function getRequest({ return prepareOptions(options) .then(interceptRequestOptions, interceptRequestError) .then(async (opts) => { - const download = await progress.estimate( - requestStream, - isBrowser, - )(opts); + const download = await progress.estimate(requestStream)(opts); if (!utils.isErrorCode(download.response.statusCode)) { // TODO: Move this to balena-image-manager diff --git a/lib/utils.ts b/lib/utils.ts index 04580ed..5aba5d9 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -13,11 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - -const { fetch: normalFetch, Headers: HeadersPonyfill } = - // eslint-disable-next-line @typescript-eslint/no-var-requires - (require('fetch-ponyfill') as typeof import('fetch-ponyfill'))({ Promise }); - import * as urlLib from 'url'; import * as qs from 'qs'; import * as errors from 'balena-errors'; @@ -31,6 +26,8 @@ import type { } from './request'; import { Readable } from 'stream'; +const nativeFetch = fetch; + const IS_BROWSER = typeof window !== 'undefined' && window !== null; /** @@ -311,7 +308,7 @@ const processRequestOptions = function (options: BalenaRequestOptions) { compress: options.gzip, signal: options.signal, body, - headers: new HeadersPonyfill(headers), + headers: new Headers(headers), mode: 'cors', redirect: options.followRedirect === false ? 'manual' : 'follow', }; @@ -399,7 +396,7 @@ const getForm = (): FormDataNodeType | FormData => { // This is the actual implementation that hides the internal `retriesRemaining` parameter async function requestAsync( - fetch: typeof normalFetch, + $fetch: typeof nativeFetch, options: BalenaRequestOptions, retriesRemaining?: number, ): Promise { @@ -409,7 +406,7 @@ async function requestAsync( } // When streaming, prefer using the native Headers object if available - if (fetch !== normalFetch && typeof Headers === 'function') { + if ($fetch !== nativeFetch && 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 @@ -451,6 +448,15 @@ async function requestAsync( const encoder = new FormDataEncoder(form as FormDataNodeType); opts.headers.set('Content-Type', encoder.headers['Content-Type']); + // future proofing in case the user has already defined this + if (!('duplex' in opts)) { + // The spec requires setting this when uploading files. + // See: https://fetch.spec.whatwg.org/#enumdef-requestduplex + // @ts-expect-error The RequestInit type in @types/node/global.d.ts does include + // the duplex property, but lib.dom.d.ts does not, so since this lib needs to run + // on both node & browsers, the types used need to satisfy both. + opts.duplex = 'half'; + } const length = encoder.headers['Content-Length']; if (length != null) { opts.headers.set('Content-Length', length); @@ -462,8 +468,8 @@ async function requestAsync( try { const requestTime = Date.now(); - let p = fetch(url, opts); - if (opts.timeout && IS_BROWSER) { + let p = $fetch(url, opts); + if (opts.timeout) { p = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('network timeout')); @@ -488,7 +494,7 @@ async function requestAsync( return response; } catch (err) { if (retriesRemaining > 0) { - return await requestAsync(fetch, options, retriesRemaining - 1); + return await requestAsync($fetch, options, retriesRemaining - 1); } throw err; } @@ -531,7 +537,7 @@ function handleAbortIfNotSupported( * @function * @protected * - * @param {Function} [fetch] - the fetch implementation, defaults to that returned by `fetch-ponyfill`. + * @param {Function} [$fetch] - the fetch implementation, defaults to native node/browser fetch. * * @description The returned function keeps partial compatibility with promisified `request` * but uses `fetch` behind the scenes. @@ -541,7 +547,7 @@ function handleAbortIfNotSupported( * utils.getRequestAsync()({ url: 'http://example.com' }).then (response) -> * console.log(response) */ -export function getRequestAsync($fetch: typeof fetch = normalFetch) { +export function getRequestAsync($fetch = nativeFetch) { return (options: BalenaRequestOptions) => requestAsync($fetch, options); } diff --git a/package.json b/package.json index 272c55e..0dacefa 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,6 @@ "dependencies": { "@balena/node-web-streams": "^0.2.3", "balena-errors": "^4.9.0", - "fetch-ponyfill": "^7.1.0", "fetch-readablestream": "^0.2.0", "form-data-encoder": "1.7.2", "formdata-node": "^4.0.0", diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index 5e52671..17ed0c1 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -1,13 +1,11 @@ import { expect } from 'chai'; import { TokenType } from 'balena-auth/build/token'; import setup from './setup'; -import * as fetchPonyfill from 'fetch-ponyfill'; import * as sinon from 'sinon'; import * as tokens from './tokens.json'; import * as utils from '../build/utils'; import type { BalenaRequestResponse } from '../build/request'; -const { Headers } = fetchPonyfill({ Promise }); const { auth } = setup(); const johnDoeFixture = tokens.johndoe;