diff --git a/config/config_DEPRECATED.ts b/config/config_DEPRECATED.ts index 745d49ba..e05e5735 100644 --- a/config/config_DEPRECATED.ts +++ b/config/config_DEPRECATED.ts @@ -736,6 +736,9 @@ function getConfigVariablesFromEnv() { personalAccessKey: env[ENVIRONMENT_VARIABLES.HUBSPOT_PERSONAL_ACCESS_KEY], portalId: parseInt(env[ENVIRONMENT_VARIABLES.HUBSPOT_PORTAL_ID] || '', 10), refreshToken: env[ENVIRONMENT_VARIABLES.HUBSPOT_REFRESH_TOKEN], + httpTimeout: env[ENVIRONMENT_VARIABLES.HTTP_TIMEOUT] + ? parseInt(env[ENVIRONMENT_VARIABLES.HTTP_TIMEOUT] as string) + : undefined, env: getValidEnv( env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] as Environment ), @@ -745,8 +748,9 @@ function getConfigVariablesFromEnv() { function generatePersonalAccessKeyConfig( portalId: number, personalAccessKey: string, - env: Environment -): { portals: Array } { + env: Environment, + httpTimeout?: number +): { portals: Array; httpTimeout?: number } { return { portals: [ { @@ -756,6 +760,7 @@ function generatePersonalAccessKeyConfig( env, }, ], + httpTimeout, }; } @@ -765,8 +770,9 @@ function generateOauthConfig( clientSecret: string, refreshToken: string, scopes: Array, - env: Environment -): { portals: Array } { + env: Environment, + httpTimeout?: number +): { portals: Array; httpTimeout?: number } { return { portals: [ { @@ -783,6 +789,7 @@ function generateOauthConfig( env, }, ], + httpTimeout, }; } @@ -816,6 +823,7 @@ export function loadConfigFromEnvironment({ portalId, refreshToken, env, + httpTimeout, } = getConfigVariablesFromEnv(); const unableToLoadEnvConfigError = 'Unable to load config from environment variables.'; @@ -825,8 +833,19 @@ export function loadConfigFromEnvironment({ return; } + if (httpTimeout && httpTimeout < MIN_HTTP_TIMEOUT) { + throw new Error( + `The HTTP timeout value ${httpTimeout} is invalid. The value must be a number greater than ${MIN_HTTP_TIMEOUT}.` + ); + } + if (personalAccessKey) { - return generatePersonalAccessKeyConfig(portalId, personalAccessKey, env); + return generatePersonalAccessKeyConfig( + portalId, + personalAccessKey, + env, + httpTimeout + ); } else if (clientId && clientSecret && refreshToken) { return generateOauthConfig( portalId, @@ -834,7 +853,8 @@ export function loadConfigFromEnvironment({ clientSecret, refreshToken, OAUTH_SCOPES.map(scope => scope.value), - env + env, + httpTimeout ); } else if (apiKey) { return generateApiKeyConfig(portalId, apiKey, env); diff --git a/constants/environments.ts b/constants/environments.ts index d2730e92..cb704fa2 100644 --- a/constants/environments.ts +++ b/constants/environments.ts @@ -12,4 +12,5 @@ export const ENVIRONMENT_VARIABLES = { HUBSPOT_PORTAL_ID: 'HUBSPOT_PORTAL_ID', HUBSPOT_REFRESH_TOKEN: 'HUBSPOT_REFRESH_TOKEN', HUBSPOT_ENVIRONMENT: 'HUBSPOT_ENVIRONMENT', + HTTP_TIMEOUT: 'HTTP_TIMEOUT', } as const; diff --git a/errors/apiErrors.ts b/errors/apiErrors.ts index a6f1570a..025d3c2b 100644 --- a/errors/apiErrors.ts +++ b/errors/apiErrors.ts @@ -19,7 +19,13 @@ export function isSpecifiedError( statusCode, category, subCategory, - }: { statusCode?: number; category?: string; subCategory?: string } + code, + }: { + statusCode?: number; + category?: string; + subCategory?: string; + code?: string; + } ): boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any const error = (err && (err.cause as AxiosError)) || err; @@ -27,8 +33,15 @@ export function isSpecifiedError( const categoryErr = !category || error.response?.data?.category === category; const subCategoryErr = !subCategory || error.response?.data?.subCategory === subCategory; + const codeError = !code || error.code === code; - return error.isAxiosError && statusCodeErr && categoryErr && subCategoryErr; + return ( + error.isAxiosError && + statusCodeErr && + categoryErr && + subCategoryErr && + codeError + ); } export function isMissingScopeError(err: Error | AxiosError): boolean { @@ -39,6 +52,10 @@ export function isGatingError(err: Error | AxiosError): boolean { return isSpecifiedError(err, { statusCode: 403, category: 'GATED' }); } +export function isTimeoutError(err: Error | AxiosError): boolean { + return isSpecifiedError(err, { code: 'ETIMEDOUT' }); +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isApiUploadValidationError(err: AxiosError): boolean { return ( diff --git a/http/__tests__/index.ts b/http/__tests__/index.ts index 9ddef4b7..cf12d8b2 100644 --- a/http/__tests__/index.ts +++ b/http/__tests__/index.ts @@ -148,6 +148,9 @@ describe('http/index', () => { params: { portalId: 123, }, + transitional: { + clarifyTimeoutError: true, + }, }); }); it('adds authorization header when using a user token', async () => { @@ -182,6 +185,9 @@ describe('http/index', () => { params: { portalId: 123, }, + transitional: { + clarifyTimeoutError: true, + }, }); }); @@ -215,6 +221,9 @@ describe('http/index', () => { portalId: 123, hapikey: 'abc', }, + transitional: { + clarifyTimeoutError: true, + }, }); }); }); diff --git a/http/getAxiosConfig.ts b/http/getAxiosConfig.ts index ada6b88b..289b264c 100644 --- a/http/getAxiosConfig.ts +++ b/http/getAxiosConfig.ts @@ -9,6 +9,10 @@ export const DEFAULT_USER_AGENT_HEADERS = { 'User-Agent': `HubSpot Local Dev Lib/${version}`, }; +const DEFAULT_TRANSITIONAL = { + clarifyTimeoutError: true, +}; + export function getAxiosConfig( options: AxiosConfigOptions ): AxiosRequestConfig { @@ -26,6 +30,7 @@ export function getAxiosConfig( ...(headers || {}), }, timeout: httpTimeout || 15000, + transitional: DEFAULT_TRANSITIONAL, ...rest, }; } diff --git a/lib/fileMapper.ts b/lib/fileMapper.ts index 33c35f9b..4b026895 100644 --- a/lib/fileMapper.ts +++ b/lib/fileMapper.ts @@ -22,6 +22,7 @@ import { FileMapperInputOptions, } from '../types/Files'; import { throwFileSystemError } from '../errors/fileSystemErrors'; +import { isTimeoutError } from '../errors/apiErrors'; import { BaseError } from '../types/Error'; import { i18n } from '../utils/lang'; @@ -264,10 +265,6 @@ async function writeFileMapperNode( return true; } -function isTimeout(err: BaseError): boolean { - return !!err && (err.status === 408 || err.code === 'ESOCKETTIMEDOUT'); -} - async function downloadFile( accountId: number, src: string, @@ -308,7 +305,7 @@ async function downloadFile( ); } catch (err) { const error = err as AxiosError; - if (isHubspot && isTimeout(error)) { + if (isHubspot && isTimeoutError(error)) { throwErrorWithMessage(`${i18nKey}.errors.assetTimeout`, {}, error); } else { throwErrorWithMessage( @@ -332,22 +329,13 @@ export async function fetchFolderFromApi( src, }); } - try { - const srcPath = isRoot ? '@root' : src; - const queryValues = getFileMapperQueryValues(mode, options); - const node = isHubspot - ? await downloadDefault(accountId, srcPath, queryValues) - : await download(accountId, srcPath, queryValues); - logger.log(i18n(`${i18nKey}.folderFetch`, { src, accountId })); - return node; - } catch (err) { - const error = err as BaseError; - if (isHubspot && isTimeout(error)) { - throwErrorWithMessage(`${i18nKey}.errors.assetTimeout`, {}, error); - } else { - throwError(error); - } - } + const srcPath = isRoot ? '@root' : src; + const queryValues = getFileMapperQueryValues(mode, options); + const node = isHubspot + ? await downloadDefault(accountId, srcPath, queryValues) + : await download(accountId, srcPath, queryValues); + logger.log(i18n(`${i18nKey}.folderFetch`, { src, accountId })); + return node; } async function downloadFolder( @@ -401,11 +389,16 @@ async function downloadFolder( throwErrorWithMessage(`${i18nKey}.errors.incompleteFetch`, { src }); } } catch (err) { - throwErrorWithMessage( - `${i18nKey}.errors.failedToFetchFolder`, - { src, dest: destPath }, - err as AxiosError - ); + const error = err as AxiosError; + if (isTimeoutError(error)) { + throwErrorWithMessage(`${i18nKey}.errors.assetTimeout`, {}, error); + } else { + throwErrorWithMessage( + `${i18nKey}.errors.failedToFetchFolder`, + { src, dest: destPath }, + err as AxiosError + ); + } } }