Skip to content

Commit

Permalink
Merge pull request #116 from HubSpot/fix-timeout-errors
Browse files Browse the repository at this point in the history
Support HTTP_TIMEOUT environment variable
  • Loading branch information
camden11 authored Mar 13, 2024
2 parents acaa2cf + 6484c6d commit 837d02f
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 34 deletions.
32 changes: 26 additions & 6 deletions config/config_DEPRECATED.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
Expand All @@ -745,8 +748,9 @@ function getConfigVariablesFromEnv() {
function generatePersonalAccessKeyConfig(
portalId: number,
personalAccessKey: string,
env: Environment
): { portals: Array<CLIAccount_DEPRECATED> } {
env: Environment,
httpTimeout?: number
): { portals: Array<CLIAccount_DEPRECATED>; httpTimeout?: number } {
return {
portals: [
{
Expand All @@ -756,6 +760,7 @@ function generatePersonalAccessKeyConfig(
env,
},
],
httpTimeout,
};
}

Expand All @@ -765,8 +770,9 @@ function generateOauthConfig(
clientSecret: string,
refreshToken: string,
scopes: Array<string>,
env: Environment
): { portals: Array<OAuthAccount_DEPRECATED> } {
env: Environment,
httpTimeout?: number
): { portals: Array<OAuthAccount_DEPRECATED>; httpTimeout?: number } {
return {
portals: [
{
Expand All @@ -783,6 +789,7 @@ function generateOauthConfig(
env,
},
],
httpTimeout,
};
}

Expand Down Expand Up @@ -816,6 +823,7 @@ export function loadConfigFromEnvironment({
portalId,
refreshToken,
env,
httpTimeout,
} = getConfigVariablesFromEnv();
const unableToLoadEnvConfigError =
'Unable to load config from environment variables.';
Expand All @@ -825,16 +833,28 @@ 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,
clientId,
clientSecret,
refreshToken,
OAUTH_SCOPES.map(scope => scope.value),
env
env,
httpTimeout
);
} else if (apiKey) {
return generateApiKeyConfig(portalId, apiKey, env);
Expand Down
1 change: 1 addition & 0 deletions constants/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
21 changes: 19 additions & 2 deletions errors/apiErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,29 @@ 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<any>)) || err;
const statusCodeErr = !statusCode || error.response?.status === statusCode;
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 {
Expand All @@ -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<any>): boolean {
return (
Expand Down
9 changes: 9 additions & 0 deletions http/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ describe('http/index', () => {
params: {
portalId: 123,
},
transitional: {
clarifyTimeoutError: true,
},
});
});
it('adds authorization header when using a user token', async () => {
Expand Down Expand Up @@ -182,6 +185,9 @@ describe('http/index', () => {
params: {
portalId: 123,
},
transitional: {
clarifyTimeoutError: true,
},
});
});

Expand Down Expand Up @@ -215,6 +221,9 @@ describe('http/index', () => {
portalId: 123,
hapikey: 'abc',
},
transitional: {
clarifyTimeoutError: true,
},
});
});
});
Expand Down
5 changes: 5 additions & 0 deletions http/getAxiosConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,6 +30,7 @@ export function getAxiosConfig(
...(headers || {}),
},
timeout: httpTimeout || 15000,
transitional: DEFAULT_TRANSITIONAL,
...rest,
};
}
45 changes: 19 additions & 26 deletions lib/fileMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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
);
}
}
}

Expand Down

0 comments on commit 837d02f

Please sign in to comment.