Skip to content

Commit

Permalink
Drop fetch-ponyfill in favor of native fetch
Browse files Browse the repository at this point in the history
Change-type: minor
  • Loading branch information
thgreasi committed Dec 4, 2024
1 parent 1c55f01 commit 1b866a8
Show file tree
Hide file tree
Showing 5 changed files with 22 additions and 73 deletions.
55 changes: 2 additions & 53 deletions lib/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,10 @@ export interface BalenaRequestPassThroughStream extends Stream.PassThrough {
* stream.on 'progress', (state) ->
* console.log(state)
*/
export function estimate(
requestAsync?: ReturnType<typeof getRequestAsync>,
isBrowser?: boolean,
) {
export function estimate(requestAsync: ReturnType<typeof getRequestAsync>) {
return async function (
options: BalenaRequestOptions,
): Promise<BalenaRequestPassThroughStream> {
if (requestAsync == null) {
requestAsync = utils.getRequestAsync();
}

options.gzip = false;
options.headers!['Accept-Encoding'] = 'gzip, deflate';

Expand Down Expand Up @@ -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));
Expand Down
5 changes: 1 addition & 4 deletions lib/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 19 additions & 13 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +26,8 @@ import type {
} from './request';
import { Readable } from 'stream';

const nativeFetch = fetch;

const IS_BROWSER = typeof window !== 'undefined' && window !== null;

/**
Expand Down Expand Up @@ -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',
};
Expand Down Expand Up @@ -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<BalenaRequestResponse> {
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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'));
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand All @@ -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);
}

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down

0 comments on commit 1b866a8

Please sign in to comment.