UNPKG

8.02 kBJavaScriptView Raw
1const { HubSpotAuthError } = require('@hubspot/api-auth-lib/Errors');
2const { logger } = require('./logger');
3
4const isApiStatusCodeError = err =>
5 err.name === 'StatusCodeError' ||
6 (err.statusCode >= 100 && err.statusCode < 600);
7const isApiUploadValidationError = err =>
8 !!(
9 err.statusCode === 400 &&
10 err.response &&
11 err.response.body &&
12 (err.response.body.message || err.response.body.errors)
13 );
14const isSystemError = err =>
15 err.errno != null && err.code != null && err.syscall != null;
16const isFatalError = err => err instanceof HubSpotAuthError;
17const contactSupportString =
18 'Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.';
19
20const parseValidationErrors = (responseBody = {}) => {
21 const errorMessages = [];
22
23 const { errors, message } = responseBody;
24
25 if (message) {
26 errorMessages.push(message);
27 }
28
29 if (errors) {
30 const specificErrors = errors.map(error => {
31 let errorMessage = error.message;
32 if (error.errorTokens && error.errorTokens.line) {
33 errorMessage = `line ${error.errorTokens.line}: ${errorMessage}`;
34 }
35 return errorMessage;
36 });
37 errorMessages.push(...specificErrors);
38 }
39
40 return errorMessages;
41};
42
43// TODO: Make these TS interfaces
44class ErrorContext {
45 constructor(props = {}) {
46 /** @type {number} */
47 this.portalId = props.portalId;
48 }
49}
50
51class ApiErrorContext extends ErrorContext {
52 constructor(props = {}) {
53 super(props);
54 /** @type {string} */
55 this.request = props.request || '';
56 /** @type {string} */
57 this.payload = props.payload || '';
58 }
59}
60
61class FileSystemErrorContext extends ErrorContext {
62 constructor(props = {}) {
63 super(props);
64 /** @type {string} */
65 this.filepath = props.filepath || '';
66 /** @type {boolean} */
67 this.read = !!props.read;
68 /** @type {boolean} */
69 this.write = !!props.write;
70 }
71}
72
73/**
74 * Logs (debug) the error and context objects.
75 *
76 * @param {SystemError} error
77 * @param {ErrorContext} context
78 */
79function debugErrorAndContext(error, context) {
80 logger.debug('Error: %o', error);
81 logger.debug('Context: %o', context);
82}
83
84/**
85 * Logs a SystemError
86 * @see {@link https://nodejs.org/api/errors.html#errors_class_systemerror}
87 *
88 * @param {SystemError} error
89 * @param {ErrorContext} context
90 */
91function logSystemError(error, context) {
92 logger.error(`A system error has occurred: ${error.message}`);
93 debugErrorAndContext(error, context);
94}
95
96/**
97 * Logs a message for an error instance of type not asserted.
98 *
99 * @param {Error|SystemError|Object} error
100 * @param {ErrorContext} context
101 */
102function logErrorInstance(error, context) {
103 // SystemError
104 if (isSystemError(error)) {
105 logSystemError(error, context);
106 return;
107 }
108 if (error instanceof Error || error.message || error.reason) {
109 // Error or Error subclass
110 const name = error.name || 'Error';
111 const message = [`A ${name} has occurred.`];
112 [error.message, error.reason].forEach(msg => {
113 if (msg) {
114 message.push(msg);
115 }
116 });
117 logger.error(message.join(' '));
118 } else {
119 // Unknown errors
120 logger.error(`An unknown error has occurred.`);
121 }
122 debugErrorAndContext(error, context);
123}
124
125/**
126 * @param {Error} error
127 * @param {ApiErrorContext} context
128 */
129function logValidationErrors(error, context) {
130 const { response = {} } = error;
131 const validationErrors = parseValidationErrors(response.body);
132 if (validationErrors.length) {
133 validationErrors.forEach(err => {
134 logger.error(err);
135 });
136 }
137 debugErrorAndContext(error, context);
138}
139
140/**
141 * Message segments for API messages.
142 *
143 * @enum {string}
144 */
145const ApiMethodVerbs = {
146 DEFAULT: 'request',
147 DELETE: 'delete',
148 GET: 'request',
149 PATCH: 'update',
150 POST: 'post',
151 PUT: 'update',
152};
153
154/**
155 * Message segments for API messages.
156 *
157 * @enum {string}
158 */
159const ApiMethodPrepositions = {
160 DEFAULT: 'for',
161 DELETE: 'of',
162 GET: 'for',
163 PATCH: 'to',
164 POST: 'to',
165 PUT: 'to',
166};
167
168/**
169 * Logs messages for an error instance resulting from API interaction.
170 *
171 * @param {StatusCodeError} error
172 * @param {ApiErrorContext} context
173 */
174function logApiStatusCodeError(error, context) {
175 const { statusCode } = error;
176 const { method } = error.options || {};
177 const isPutOrPost = method === 'PUT' || method === 'POST';
178 const action = ApiMethodVerbs[method] || ApiMethodVerbs.DEFAULT;
179 const preposition =
180 ApiMethodPrepositions[method] || ApiMethodPrepositions.DEFAULT;
181 let messageDetail = '';
182 {
183 const request = context.request
184 ? `${action} ${preposition} "${context.request}"`
185 : action;
186 messageDetail = `${request} in portal ${context.portalId}`;
187 }
188 const errorMessage = [];
189 if (isPutOrPost && context.payload) {
190 errorMessage.push(`Unable to upload "${context.payload}".`);
191 }
192 switch (statusCode) {
193 case 400:
194 errorMessage.push(`The ${messageDetail} was bad.`);
195 break;
196 case 401:
197 errorMessage.push(`The ${messageDetail} was unauthorized.`);
198 break;
199 case 403:
200 errorMessage.push(`The ${messageDetail} was forbidden.`);
201 break;
202 case 404:
203 if (context.request) {
204 errorMessage.push(
205 `The ${action} failed because "${context.request}" was not found in portal ${context.portalId}.`
206 );
207 } else {
208 errorMessage.push(`The ${messageDetail} was not found.`);
209 }
210 break;
211 case 503:
212 errorMessage.push(
213 `The ${messageDetail} could not be handled at this time. ${contactSupportString}`
214 );
215 break;
216 default:
217 if (statusCode >= 500 && statusCode < 600) {
218 errorMessage.push(
219 `The ${messageDetail} failed due to a server error. ${contactSupportString}`
220 );
221 } else if (statusCode >= 400 && statusCode < 500) {
222 errorMessage.push(`The ${messageDetail} failed due to a client error.`);
223 } else {
224 errorMessage.push(`The ${messageDetail} failed.`);
225 }
226 break;
227 }
228 if (error.error && error.error.message) {
229 errorMessage.push(error.error.message);
230 }
231 logger.error(errorMessage.join(' '));
232 debugErrorAndContext(error, context);
233}
234
235/**
236 * Logs a message for an error instance resulting from API interaction.
237 *
238 * @param {Error|SystemError|Object} error
239 * @param {ApiErrorContext} context
240 */
241function logApiErrorInstance(error, context) {
242 // StatusCodeError
243 if (isApiStatusCodeError(error)) {
244 logApiStatusCodeError(error, context);
245 return;
246 }
247 logErrorInstance(error, context);
248}
249
250/**
251 * Logs a message for an error instance resulting from filemapper API upload.
252 *
253 * @param {Error|SystemError|Object} error
254 * @param {ApiErrorContext} context
255 */
256function logApiUploadErrorInstance(error, context) {
257 if (isApiUploadValidationError(error)) {
258 logValidationErrors(error, context);
259 return;
260 }
261 logApiErrorInstance(error, context);
262}
263
264/**
265 * Logs a message for an error instance resulting from filesystem interaction.
266 *
267 * @param {Error|SystemError|Object} error
268 * @param {FileSystemErrorContext} context
269 */
270function logFileSystemErrorInstance(error, context) {
271 let fileAction = '';
272 if (context.read) {
273 fileAction = 'reading from';
274 } else if (context.write) {
275 fileAction = 'writing to';
276 } else {
277 fileAction = 'accessing';
278 }
279 const filepath = context.filepath
280 ? `"${context.filepath}"`
281 : 'a file or folder';
282 const message = [`An error occurred while ${fileAction} ${filepath}.`];
283 // Many `fs` errors will be `SystemError`s
284 if (isSystemError(error)) {
285 message.push(`This is the result of a system error: ${error.message}`);
286 }
287 logger.error(message.join(' '));
288 debugErrorAndContext(error, context);
289}
290
291module.exports = {
292 ErrorContext,
293 ApiErrorContext,
294 FileSystemErrorContext,
295 isFatalError,
296 parseValidationErrors,
297 logErrorInstance,
298 logApiErrorInstance,
299 logApiUploadErrorInstance,
300 logFileSystemErrorInstance,
301};