From f6ec22ac49215c0cfd0e3a684c00d26531682875 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Fri, 16 Apr 2021 13:31:59 -0700 Subject: [PATCH] improvement(serverless-api): add request url to API error logs We wrap error events from the API/Got into a dedicated class. The URL helps support to debug issues but wasn't in the debug logs since this change. This commit adds the URL back fix #167 --- .../src/utils/__tests__/error.test.ts | 74 ++++++++++++++++++- packages/serverless-api/src/utils/error.ts | 9 ++- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/serverless-api/src/utils/__tests__/error.test.ts b/packages/serverless-api/src/utils/__tests__/error.test.ts index 29eaadbd..f64b8fdd 100644 --- a/packages/serverless-api/src/utils/__tests__/error.test.ts +++ b/packages/serverless-api/src/utils/__tests__/error.test.ts @@ -1,16 +1,23 @@ import nock from 'nock'; import util from 'util'; +import { AccountSidConfig, UsernameConfig } from '../../../dist'; import { createGotClient } from '../../client'; +import { + DEFAULT_TEST_CLIENT_CONFIG, + DEFAULT_TEST_CLIENT_CONFIG_USERNAME_PASSWORD, +} from '../../__fixtures__/base-fixtures'; import { ClientApiError } from '../error'; -import { DEFAULT_TEST_CLIENT_CONFIG } from '../../__fixtures__/base-fixtures'; describe('ClientApiError', () => { let apiNock = nock('https://serverless.twilio.com'); - let config = DEFAULT_TEST_CLIENT_CONFIG; + let config: + | UsernameConfig + | AccountSidConfig = DEFAULT_TEST_CLIENT_CONFIG_USERNAME_PASSWORD; let apiClient = createGotClient(config); beforeEach(() => { apiNock = nock('https://serverless.twilio.com'); + config = DEFAULT_TEST_CLIENT_CONFIG_USERNAME_PASSWORD; apiClient = createGotClient(config); }); @@ -40,12 +47,73 @@ describe('ClientApiError', () => { status: 400, }); + config = DEFAULT_TEST_CLIENT_CONFIG; + apiClient = createGotClient(config); + + try { + const resp = await apiClient.get('Services'); + } catch (err) { + const newError = new ClientApiError(err); + const stringVersion = util.inspect(newError); + expect(stringVersion.includes(config.authToken)).toBe(false); + } + }); + + test('does not contain any password', async () => { + const scope = apiNock.get('/v1/Services').reply(400, { + code: 20001, + message: 'Services are limited to less than or equal to 9000', + more_info: 'https://www.twilio.com/docs/errors/20001', + status: 400, + }); + try { const resp = await apiClient.get('Services'); } catch (err) { const newError = new ClientApiError(err); const stringVersion = util.inspect(newError); - expect(stringVersion).not.toContain(config.authToken); + expect(stringVersion.includes((config as UsernameConfig).password)).toBe( + false + ); + } + }); + + test('contains URL and status code for basic HTTP Errors', async () => { + const scope = apiNock.get('/v1/Services/ZS1111').reply(404, {}); + + try { + const resp = await apiClient.get('Services/ZS1111'); + } catch (err) { + const newError = new ClientApiError(err); + const expectedUrl = 'https://serverless.twilio.com/v1/Services/ZS1111'; + expect(newError.code).toEqual(404); + expect(newError.url).toEqual(expectedUrl); + + const stringVersion = util.inspect(newError); + expect(stringVersion.includes(expectedUrl)).toBe(true); + expect(stringVersion.includes('404')).toBe(true); + } + }); + + test('contains URL and API code for Twilio API Errrors', async () => { + const scope = apiNock.get('/v1/Services/ZS1111').reply(400, { + code: 20001, + message: 'Services are limited to less than or equal to 9000', + more_info: 'https://www.twilio.com/docs/errors/20001', + status: 400, + }); + + try { + const resp = await apiClient.get('Services/ZS1111'); + } catch (err) { + const newError = new ClientApiError(err); + const expectedUrl = 'https://serverless.twilio.com/v1/Services/ZS1111'; + expect(newError.code).toEqual(20001); + expect(newError.url).toEqual(expectedUrl); + + const stringVersion = util.inspect(newError); + expect(stringVersion.includes(expectedUrl)).toBe(true); + expect(stringVersion.includes('20001')).toBe(true); } }); }); diff --git a/packages/serverless-api/src/utils/error.ts b/packages/serverless-api/src/utils/error.ts index bc4b0ca4..bd7c242f 100644 --- a/packages/serverless-api/src/utils/error.ts +++ b/packages/serverless-api/src/utils/error.ts @@ -22,7 +22,7 @@ function toTwilioApiError(response: unknown): TwilioApiError | undefined { * Explictly removes username and password from a URL. * @param unfilteredUrl any URL string */ -function filterUrl(unfilteredUrl: string | undefined): string { +export function filterUrl(unfilteredUrl: string | undefined): string { if (!unfilteredUrl) { return ''; } @@ -49,8 +49,9 @@ export function convertApiErrorsAndThrow(err: any): never { * An Error wrapper to provide more useful error information without exposing credentials */ export class ClientApiError extends Error { - public details?: TwilioApiError & { request_url?: string }; + public details?: TwilioApiError; public code?: number | string; + public url?: string; constructor(requestError: RequestError) { const twilioApiErrorInfo = toTwilioApiError(requestError.response?.body); @@ -59,13 +60,13 @@ export class ClientApiError extends Error { Error.captureStackTrace(this, this.constructor); this.name = requestError.name; this.message = message; - this.code = twilioApiErrorInfo?.code || requestError.code; + this.code = twilioApiErrorInfo?.code || requestError.response?.statusCode; + this.url = filterUrl(requestError.response?.requestUrl); if (requestError.name === 'HTTPError' && twilioApiErrorInfo) { this.name = 'TwilioApiError'; this.details = { ...twilioApiErrorInfo, - request_url: filterUrl(requestError.response?.requestUrl), }; } }