UNPKG

@shopify/cli-kit

Version:

A set of utilities, interfaces, and models that are common across all the platform features

209 lines • 8.44 kB
/* eslint-disable @typescript-eslint/no-base-to-string */ import { dirname } from './path.js'; import { createFileWriteStream, fileExistsSync, mkdirSync, unlinkFileSync } from './fs.js'; import { runWithTimer } from './metadata.js'; import { maxRequestTimeForNetworkCallsMs, skipNetworkLevelRetry } from './environment.js'; import { httpsAgent, sanitizedHeadersOutput } from '../../private/node/api/headers.js'; import { sanitizeURL } from '../../private/node/api/urls.js'; import { outputContent, outputDebug, outputToken } from '../../public/node/output.js'; import { simpleRequestWithDebugLog } from '../../private/node/api.js'; import { DEFAULT_MAX_TIME_MS } from '../../private/node/sleep-with-backoff.js'; import FormData from 'form-data'; import nodeFetch from 'node-fetch'; export { FetchError, Request, Response } from 'node-fetch'; /** * Create a new FormData object. * * @returns A FormData object. */ export function formData() { return new FormData(); } /** * Specify the behaviour of a network request. * * - default: Requests are automatically retried, and are subject to automatic cancellation if they're taking too long. * This is generally desirable. * - non-blocking: Requests are not retried if they fail with a network error, and are automatically cancelled if * they're taking too long. This is good for throwaway requests, like polling or tracking. * - slow-request: Requests are not retried if they fail with a network error, and are not automatically cancelled. * This is good for slow requests that should be give the chance to complete, and are unlikely to be safe to retry. * * Some request behaviours may be de-activated by the environment, and this function takes care of that concern. You * can also provide a customised request behaviour. * * @param preset - The preset to use. * @param env - Process environment variables. * @returns A request behaviour object. */ export function requestMode(preset = 'default', env = process.env) { const networkLevelRetryIsSupported = !skipNetworkLevelRetry(env); switch (preset) { case 'default': return { useNetworkLevelRetry: networkLevelRetryIsSupported, maxRetryTimeMs: DEFAULT_MAX_TIME_MS, useAbortSignal: true, timeoutMs: maxRequestTimeForNetworkCallsMs(env), }; case 'non-blocking': return { useNetworkLevelRetry: false, useAbortSignal: true, timeoutMs: maxRequestTimeForNetworkCallsMs(env), }; case 'slow-request': return { useNetworkLevelRetry: false, useAbortSignal: false, }; } return { ...preset, useNetworkLevelRetry: networkLevelRetryIsSupported && preset.useNetworkLevelRetry, }; } /** * Create an AbortSignal for automatic request cancellation, from a request behaviour. * * @param behaviour - The request behaviour. * @returns An AbortSignal. */ export function abortSignalFromRequestBehaviour(behaviour) { let signal; if (behaviour.useAbortSignal === true) { signal = AbortSignal.timeout(behaviour.timeoutMs); } else if (behaviour.useAbortSignal && typeof behaviour.useAbortSignal === 'function') { signal = behaviour.useAbortSignal(); } else if (behaviour.useAbortSignal) { signal = behaviour.useAbortSignal; } return signal; } async function innerFetch({ url, behaviour, init, logRequest, useHttpsAgent }) { if (logRequest) { outputDebug(outputContent `Sending ${init?.method ?? 'GET'} request to URL ${sanitizeURL(url.toString())} With request headers: ${sanitizedHeadersOutput((init?.headers ?? {}))} `); } let agent; if (useHttpsAgent) { agent = await httpsAgent(); } const request = async () => { // each time we make the request, we need to potentially reset the abort signal, as the request logic may make // the same request multiple times. let signal = abortSignalFromRequestBehaviour(behaviour); // it's possible to provide a signal through the request's init structure. if (init?.signal) { signal = init.signal; } return nodeFetch(url, { ...init, agent, signal }); }; return runWithTimer('cmd_all_timing_network_ms')(async () => { return simpleRequestWithDebugLog({ url: url.toString(), request, ...behaviour, }); }); } /** * An interface that abstracts way node-fetch. When Node has built-in * support for "fetch" in the standard library, we can drop the node-fetch * dependency from here. * Note that we are exposing types from "node-fetch". The reason being is that * they are consistent with the Web API so if we drop node-fetch in the future * it won't require changes from the callers. * * The CLI's fetch function supports special behaviours, like automatic retries. These are disabled by default through * this function. * * @param url - This defines the resource that you wish to fetch. * @param init - An object containing any custom settings that you want to apply to the request. * @param preferredBehaviour - A request behaviour object that overrides the default behaviour. * @returns A promise that resolves with the response. */ export async function fetch(url, init, preferredBehaviour) { const options = { url, init, logRequest: false, useHttpsAgent: false, // all special behaviours are disabled by default behaviour: preferredBehaviour ? requestMode(preferredBehaviour) : requestMode('non-blocking'), }; return innerFetch(options); } /** * A fetch function to use with Shopify services. The function ensures the right * TLS configuragion is used based on the environment in which the service is running * (e.g. Local). NB: headers/auth are the responsibility of the caller. * * By default, the CLI's fetch function's special behaviours, like automatic retries, are enabled. * * @param url - This defines the resource that you wish to fetch. * @param init - An object containing any custom settings that you want to apply to the request. * @param preferredBehaviour - A request behaviour object that overrides the default behaviour. * @returns A promise that resolves with the response. */ export async function shopifyFetch(url, init, preferredBehaviour) { const options = { url, init, logRequest: true, useHttpsAgent: true, // special behaviours enabled by default behaviour: preferredBehaviour ? requestMode(preferredBehaviour) : requestMode(), }; return innerFetch(options); } /** * Download a file from a URL to a local path. * * @param url - The URL to download from. * @param to - The local path to download to. * @returns - A promise that resolves with the local path. */ export function downloadFile(url, to) { const sanitizedUrl = sanitizeURL(url); outputDebug(`Downloading ${sanitizedUrl} to ${to}`); return runWithTimer('cmd_all_timing_network_ms')(() => { return new Promise((resolve, reject) => { if (!fileExistsSync(dirname(to))) { mkdirSync(dirname(to)); } const file = createFileWriteStream(to); // if we can't remove the file for some reason (seen on windows), that's ok -- it's in a temporary directory const tryToRemoveFile = () => { try { unlinkFileSync(to); // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { outputDebug(outputContent `Failed to remove file ${outputToken.path(to)}: ${outputToken.raw(String(err))}`); } }; file.on('finish', () => { file.close(); resolve(to); }); file.on('error', (err) => { tryToRemoveFile(); reject(err); }); nodeFetch(url, { redirect: 'follow' }) .then((res) => { res.body?.pipe(file); }) .catch((err) => { tryToRemoveFile(); reject(err); }); }); }); } //# sourceMappingURL=http.js.map