Skip to content

Commit a62daf7

Browse files
fix: Use isolated axios instance to prevent multiple error wrapping (#355)
* fix: Don't wrap HubSpotHTTPError * chore: bump npm-scripts version * fix: Use axios instances * fix: Use axios instance for all requests * fix: Use httpClient in trackUsage. Update tests * refactor: Move interceptor to client
1 parent a972c19 commit a62daf7

File tree

11 files changed

+111
-98
lines changed

11 files changed

+111
-98
lines changed

api/developerTestAccounts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from 'axios';
1+
import { httpClient } from '../http/client';
22
import { http } from '../http';
33
import { getAxiosConfig } from '../http/getAxiosConfig';
44
import { ENVIRONMENTS } from '../constants/environments';
@@ -88,7 +88,7 @@ export function fetchDeveloperTestAccountData(
8888
},
8989
};
9090

91-
return axios<DeveloperTestAccount>(reqWithToken);
91+
return httpClient<DeveloperTestAccount>(reqWithToken);
9292
}
9393

9494
export function installOauthAppIntoDeveloperTestAccount(

api/localDevAuth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
EnabledFeaturesResponse,
99
ScopeAuthorizationResponse,
1010
} from '../types/Accounts';
11-
import axios from 'axios';
11+
import { httpClient } from '../http/client';
1212
import { PublicAppInstallationData } from '../types/Apps';
1313
import { HubSpotPromise } from '../types/Http';
1414

@@ -31,7 +31,7 @@ export function fetchAccessToken(
3131
params: portalId ? { portalId } : {},
3232
});
3333

34-
return axios<AccessTokenResponse>({
34+
return httpClient<AccessTokenResponse>({
3535
...axiosConfig,
3636
method: 'post',
3737
});

api/sandboxHubs.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import axios, { AxiosPromise } from 'axios';
1+
import { AxiosPromise } from 'axios';
2+
import { httpClient } from '../http/client';
23
import { http } from '../http';
34
import { getAxiosConfig } from '../http/getAxiosConfig';
45
import { ENVIRONMENTS } from '../constants/environments';
@@ -63,7 +64,7 @@ export function fetchSandboxHubData(
6364
},
6465
};
6566

66-
return axios<SandboxHubData>(reqWithToken);
67+
return httpClient<SandboxHubData>(reqWithToken);
6768
}
6869

6970
export function createV2Sandbox(

http/__tests__/index.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import axios, { AxiosError } from 'axios';
1+
import { AxiosError } from 'axios';
2+
import { httpClient } from '../client';
23
import fs from 'fs-extra';
34
import moment from 'moment';
45
import {
@@ -11,7 +12,7 @@ import { version } from '../../package.json';
1112
import { HubSpotConfigAccount } from '../../types/Accounts';
1213

1314
jest.mock('fs-extra');
14-
jest.mock('axios');
15+
jest.mock('../client');
1516
jest.mock('../../config');
1617
jest.mock('../../lib/logger');
1718

@@ -27,7 +28,7 @@ jest.mock('https', () => ({
2728
}),
2829
}));
2930

30-
const mockedAxios = jest.mocked(axios);
31+
const mockedAxios = jest.mocked(httpClient);
3132
const getConfig = __getConfig as jest.MockedFunction<typeof __getConfig>;
3233
const getConfigAccountById = __getConfigAccountById as jest.MockedFunction<
3334
typeof __getConfigAccountById

http/__tests__/unauthed.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import axios from 'axios';
1+
import { httpClient } from '../client';
22
import { getConfig as __getConfig } from '../../config';
33
import { http } from '../unauthed';
44
import { version } from '../../package.json';
55

6-
jest.mock('axios');
6+
jest.mock('../client');
77
jest.mock('../../config');
88

99
jest.mock('http', () => ({
@@ -18,7 +18,7 @@ jest.mock('https', () => ({
1818
}),
1919
}));
2020

21-
const mockedAxios = jest.mocked(axios);
21+
const mockedAxios = jest.mocked(httpClient);
2222
const getConfig = __getConfig as jest.MockedFunction<typeof __getConfig>;
2323

2424
describe('http/index', () => {

http/client.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import axios, { AxiosResponse, isAxiosError } from 'axios';
2+
import { HubSpotHttpError } from '../models/HubSpotHttpError';
3+
import { logger } from '../lib/logger';
4+
import { LOCALDEVAUTH_ACCESS_TOKEN_PATH } from '../api/localDevAuth';
5+
import { FIREALARM_API_AUTH_PATH } from '../api/fireAlarm';
6+
import * as util from 'util';
7+
import { CMS_CLI_USAGE_PATH, VSCODE_USAGE_PATH } from '../lib/trackUsage';
8+
9+
// Create an isolated axios instance for this copy of local-dev-lib.
10+
// This prevents issues when multiple copies are loaded and share the global
11+
// axios, where each copy would register interceptors on the shared instance
12+
// causing errors to be wrapped multiple times.
13+
export const httpClient = axios.create();
14+
15+
const IGNORE_URLS_NETWORK_DEBUG = [
16+
LOCALDEVAUTH_ACCESS_TOKEN_PATH,
17+
CMS_CLI_USAGE_PATH,
18+
VSCODE_USAGE_PATH,
19+
FIREALARM_API_AUTH_PATH,
20+
];
21+
22+
function logRequest(response: AxiosResponse) {
23+
try {
24+
if (!process.env.HUBSPOT_NETWORK_LOGGING) {
25+
return;
26+
}
27+
28+
if (
29+
!process.env.HUBSPOT_DEBUG_LOGGING_VERBOSE &&
30+
IGNORE_URLS_NETWORK_DEBUG.some(
31+
url => response?.config?.url && response.config.url.includes(url)
32+
)
33+
) {
34+
return;
35+
}
36+
37+
logger.debug(
38+
util.inspect(
39+
{
40+
method: response.config.method,
41+
baseURL: response.config.baseURL,
42+
url: response.config.url,
43+
data: response.data,
44+
status: response.status,
45+
},
46+
false,
47+
null,
48+
true
49+
)
50+
);
51+
} catch (error) {
52+
// Ignore any errors that occur while logging the response
53+
}
54+
}
55+
56+
// Register interceptor on our isolated instance
57+
httpClient.interceptors.response.use(
58+
(response: AxiosResponse) => {
59+
logRequest(response);
60+
return response;
61+
},
62+
error => {
63+
try {
64+
if (isAxiosError(error) && error.response) {
65+
logRequest(error.response);
66+
}
67+
} catch (e) {
68+
// Ignore any errors that occur while logging the response
69+
}
70+
71+
// Wrap all axios errors in our own Error class. Attach the error
72+
// as the cause for the new error, so we maintain the stack trace
73+
return Promise.reject(
74+
new HubSpotHttpError(error.message, { cause: error })
75+
);
76+
}
77+
);

http/index.ts

Lines changed: 8 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'path';
22
import fs from 'fs-extra';
33
import contentDisposition from 'content-disposition';
4-
import axios, {
4+
import {
55
AxiosRequestConfig,
66
AxiosResponse,
77
AxiosPromise,
@@ -23,76 +23,10 @@ import {
2323
OAUTH_AUTH_METHOD,
2424
API_KEY_AUTH_METHOD,
2525
} from '../constants/auth';
26-
import { LOCALDEVAUTH_ACCESS_TOKEN_PATH } from '../api/localDevAuth';
27-
import { FIREALARM_API_AUTH_PATH } from '../api/fireAlarm';
28-
import * as util from 'util';
29-
import { CMS_CLI_USAGE_PATH, VSCODE_USAGE_PATH } from '../lib/trackUsage';
26+
import { httpClient } from './client';
3027

3128
const i18nKey = 'http.index';
3229

33-
const IGNORE_URLS_NETWORK_DEBUG = [
34-
LOCALDEVAUTH_ACCESS_TOKEN_PATH,
35-
CMS_CLI_USAGE_PATH,
36-
VSCODE_USAGE_PATH,
37-
FIREALARM_API_AUTH_PATH,
38-
];
39-
40-
function logRequest(response: AxiosResponse) {
41-
try {
42-
if (!process.env.HUBSPOT_NETWORK_LOGGING) {
43-
return;
44-
}
45-
46-
if (
47-
!process.env.HUBSPOT_DEBUG_LOGGING_VERBOSE &&
48-
IGNORE_URLS_NETWORK_DEBUG.some(
49-
url => response?.config?.url && response.config.url.includes(url)
50-
)
51-
) {
52-
return;
53-
}
54-
55-
logger.debug(
56-
util.inspect(
57-
{
58-
method: response.config.method,
59-
baseURL: response.config.baseURL,
60-
url: response.config.url,
61-
data: response.data,
62-
status: response.status,
63-
},
64-
false,
65-
null,
66-
true
67-
)
68-
);
69-
} catch (error) {
70-
// Ignore any errors that occur while logging the response
71-
}
72-
}
73-
74-
axios.interceptors.response.use(
75-
(response: AxiosResponse) => {
76-
logRequest(response);
77-
return response;
78-
},
79-
error => {
80-
try {
81-
if (isAxiosError(error) && error.response) {
82-
logRequest(error.response);
83-
}
84-
} catch (e) {
85-
// Ignore any errors that occur while logging the response
86-
}
87-
88-
// Wrap all axios errors in our own Error class. Attach the error
89-
// as the cause for the new error, so we maintain the stack trace
90-
return Promise.reject(
91-
new HubSpotHttpError(error.message, { cause: error })
92-
);
93-
}
94-
);
95-
9630
export function addUserAgentHeader(key: string, value: string) {
9731
USER_AGENTS[key] = value;
9832
}
@@ -198,39 +132,39 @@ async function getRequest<T>(
198132
const optionsWithParams = addQueryParams(rest, params);
199133
const requestConfig = await withAuth(accountId, optionsWithParams);
200134

201-
return axios<T>(requestConfig);
135+
return httpClient<T>(requestConfig);
202136
}
203137

204138
async function postRequest<T>(
205139
accountId: number,
206140
options: HttpOptions
207141
): HubSpotPromise<T> {
208142
const requestConfig = await withAuth(accountId, options);
209-
return axios<T>({ ...requestConfig, method: 'post' });
143+
return httpClient<T>({ ...requestConfig, method: 'post' });
210144
}
211145

212146
async function putRequest<T>(
213147
accountId: number,
214148
options: HttpOptions
215149
): HubSpotPromise<T> {
216150
const requestConfig = await withAuth(accountId, options);
217-
return axios<T>({ ...requestConfig, method: 'put' });
151+
return httpClient<T>({ ...requestConfig, method: 'put' });
218152
}
219153

220154
async function patchRequest<T>(
221155
accountId: number,
222156
options: HttpOptions
223157
): HubSpotPromise<T> {
224158
const requestConfig = await withAuth(accountId, options);
225-
return axios<T>({ ...requestConfig, method: 'patch' });
159+
return httpClient<T>({ ...requestConfig, method: 'patch' });
226160
}
227161

228162
async function deleteRequest<T>(
229163
accountId: number,
230164
options: HttpOptions
231165
): HubSpotPromise<T> {
232166
const requestConfig = await withAuth(accountId, options);
233-
return axios<T>({ ...requestConfig, method: 'delete' });
167+
return httpClient<T>({ ...requestConfig, method: 'delete' });
234168
}
235169

236170
function createGetRequestStream(contentType: string) {
@@ -246,7 +180,7 @@ function createGetRequestStream(contentType: string) {
246180
return new Promise<AxiosResponse>(async (resolve, reject) => {
247181
try {
248182
const { headers, ...opts } = await withAuth(accountId, axiosConfig);
249-
const res = await axios({
183+
const res = await httpClient({
250184
method: 'get',
251185
...opts,
252186
headers: {

http/unauthed.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from 'axios';
1+
import { httpClient } from './client';
22
import { getAxiosConfig } from './getAxiosConfig';
33
import { addQueryParams } from './addQueryParams';
44
import { HttpOptions, HubSpotPromise } from '../types/Http';
@@ -8,27 +8,27 @@ async function getRequest<T>(options: HttpOptions): HubSpotPromise<T> {
88
const optionsWithParams = addQueryParams(rest, params);
99
const requestConfig = await getAxiosConfig(optionsWithParams);
1010

11-
return axios<T>(requestConfig);
11+
return httpClient<T>(requestConfig);
1212
}
1313

1414
async function postRequest<T>(options: HttpOptions): HubSpotPromise<T> {
1515
const requestConfig = await getAxiosConfig(options);
16-
return axios<T>({ ...requestConfig, method: 'post' });
16+
return httpClient<T>({ ...requestConfig, method: 'post' });
1717
}
1818

1919
async function putRequest<T>(options: HttpOptions): HubSpotPromise<T> {
2020
const requestConfig = await getAxiosConfig(options);
21-
return axios<T>({ ...requestConfig, method: 'put' });
21+
return httpClient<T>({ ...requestConfig, method: 'put' });
2222
}
2323

2424
async function patchRequest<T>(options: HttpOptions): HubSpotPromise<T> {
2525
const requestConfig = await getAxiosConfig(options);
26-
return axios<T>({ ...requestConfig, method: 'patch' });
26+
return httpClient<T>({ ...requestConfig, method: 'patch' });
2727
}
2828

2929
async function deleteRequest<T>(options: HttpOptions): HubSpotPromise<T> {
3030
const requestConfig = await getAxiosConfig(options);
31-
return axios<T>({ ...requestConfig, method: 'delete' });
31+
return httpClient<T>({ ...requestConfig, method: 'delete' });
3232
}
3333

3434
export const http = {

lib/__tests__/trackUsage.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from 'axios';
1+
import { httpClient } from '../../http/client';
22
import {
33
trackUsage,
44
CMS_CLI_USAGE_PATH,
@@ -14,12 +14,12 @@ import { FILE_MAPPER_API_PATH } from '../../api/fileMapper';
1414
import { logger } from '../logger';
1515
import { http } from '../../http';
1616

17-
jest.mock('axios');
1817
jest.mock('../../config');
1918
jest.mock('../logger');
2019
jest.mock('../../http');
20+
jest.mock('../../http/client');
2121

22-
const mockedAxios = jest.mocked(axios);
22+
const mockedAxios = jest.mocked(httpClient);
2323
const mockedLogger = jest.mocked(logger);
2424
const mockedHttp = jest.mocked(http);
2525
const getConfigAccountById = __getConfigAccountById as jest.MockedFunction<

lib/__tests__/watch.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { watch } from '../cms/watch';
66
import { CMS_PUBLISH_MODE } from '../../constants/files';
77

88
jest.mock('chokidar');
9-
jest.mock('axios');
9+
jest.mock('../../http/client');
1010
jest.mock('p-queue');
1111
jest.mock('../cms/uploadFolder');
1212

0 commit comments

Comments
 (0)