From ae2946ce26007366e9e1a53e146c91e419e5fe51 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Thu, 10 Aug 2023 11:42:51 -0700 Subject: [PATCH 01/15] feat: add errors and unit tests to check for missing errors --- lib/twilio/errors/generated.ts | 128 +++++++++++++++++++++++++++++++++ lib/twilio/errors/index.ts | 14 +++- scripts/errors.js | 15 ++++ tests/unit/error.ts | 53 +++++++------- 4 files changed, 184 insertions(+), 26 deletions(-) diff --git a/lib/twilio/errors/generated.ts b/lib/twilio/errors/generated.ts index 1bcbe4c8..57753568 100644 --- a/lib/twilio/errors/generated.ts +++ b/lib/twilio/errors/generated.ts @@ -129,6 +129,130 @@ export namespace ClientErrors { this.originalError = originalError; } } + + export class NotFound extends TwilioError { + causes: string[] = [ + 'The outbound call was made to an invalid phone number.', + 'The TwiML application sid is missing a Voice URL.', + ]; + code: number = 31404; + description: string = 'Not Found (HTTP/SIP)'; + explanation: string = 'The server has not found anything matching the request.'; + name: string = 'NotFound'; + solutions: string[] = [ + 'Ensure the phone number dialed is valid.', + 'Ensure the TwiML application is configured correctly with a Voice URL link.', + ]; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, ClientErrors.NotFound.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class TemporarilyUnavailable extends TwilioError { + causes: string[] = []; + code: number = 31480; + description: string = 'Temporarily Unavailable (SIP)'; + explanation: string = 'The callee is currently unavailable.'; + name: string = 'TemporarilyUnavailable'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, ClientErrors.TemporarilyUnavailable.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class BusyHere extends TwilioError { + causes: string[] = []; + code: number = 31486; + description: string = 'Busy Here (SIP)'; + explanation: string = 'The callee is busy.'; + name: string = 'BusyHere'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, ClientErrors.BusyHere.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } +} + +export namespace SIPServerErrors { + export class Decline extends TwilioError { + causes: string[] = []; + code: number = 31603; + description: string = 'Decline (SIP)'; + explanation: string = 'The callee does not wish to participate in the call.'; + name: string = 'Decline'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, SIPServerErrors.Decline.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } } export namespace GeneralErrors { @@ -605,6 +729,10 @@ export const errorsByCode: ReadonlyMap = new Map([ [ 20104, AuthorizationErrors.AccessTokenExpired ], [ 20151, AuthorizationErrors.AuthenticationFailed ], [ 31400, ClientErrors.BadRequest ], + [ 31404, ClientErrors.NotFound ], + [ 31480, ClientErrors.TemporarilyUnavailable ], + [ 31486, ClientErrors.BusyHere ], + [ 31603, SIPServerErrors.Decline ], [ 31000, GeneralErrors.UnknownError ], [ 31005, GeneralErrors.ConnectionError ], [ 31008, GeneralErrors.CallCancelledError ], diff --git a/lib/twilio/errors/index.ts b/lib/twilio/errors/index.ts index 4f52c031..b2b9df50 100644 --- a/lib/twilio/errors/index.ts +++ b/lib/twilio/errors/index.ts @@ -8,8 +8,10 @@ import { ClientErrors, errorsByCode, GeneralErrors, + MalformedRequestErrors, MediaErrors, SignalingErrors, + SIPServerErrors, TwilioError, UserMediaErrors, } from './generated'; @@ -50,13 +52,23 @@ export function hasErrorByCode(code: number): boolean { return errorsByCode.has(code); } -// All errors we want to throw or emit locally in the SDK need to be passed through here. +/** + * All errors we want to throw or emit locally in the SDK need to be passed + * through here. + * + * They need to first be defined in the `USED_ERRORS` list. See: + * ``` + * scripts/errors.js + * ``` + */ export { AuthorizationErrors, ClientErrors, GeneralErrors, + MalformedRequestErrors, MediaErrors, SignalingErrors, + SIPServerErrors, TwilioError, UserMediaErrors, }; diff --git a/scripts/errors.js b/scripts/errors.js index cac6b365..f9d18f40 100644 --- a/scripts/errors.js +++ b/scripts/errors.js @@ -1,6 +1,13 @@ const fs = require('fs'); const VoiceErrors = require('@twilio/voice-errors'); +/** + * Ensure that the namespaces defined here are imported and exported from the + * generated file at: + * ``` + * lib/twilio/errors/index.ts + * ``` + */ const USED_ERRORS = [ 'AuthorizationErrors.AccessTokenExpired', 'AuthorizationErrors.AccessTokenInvalid', @@ -8,6 +15,9 @@ const USED_ERRORS = [ 'AuthorizationErrors.PayloadSizeExceededError', 'AuthorizationErrors.RateExceededError', 'ClientErrors.BadRequest', + 'ClientErrors.BusyHere', + 'ClientErrors.NotFound', + 'ClientErrors.TemporarilyUnavailable', 'GeneralErrors.CallCancelledError', 'GeneralErrors.ConnectionError', 'GeneralErrors.TransportError', @@ -18,6 +28,7 @@ const USED_ERRORS = [ 'MediaErrors.ConnectionError', 'SignalingErrors.ConnectionDisconnected', 'SignalingErrors.ConnectionError', + 'SIPServerErrors.Decline', 'UserMediaErrors.PermissionDeniedError', 'UserMediaErrors.AcquisitionFailedError', ]; @@ -107,3 +118,7 @@ export const errorsByCode: ReadonlyMap = new Map([ Object.freeze(errorsByCode);\n`; fs.writeFileSync('./lib/twilio/errors/generated.ts', output, 'utf8'); + +module.exports = { + USED_ERRORS, +}; diff --git a/tests/unit/error.ts b/tests/unit/error.ts index 9729ac62..ac25c333 100644 --- a/tests/unit/error.ts +++ b/tests/unit/error.ts @@ -1,14 +1,13 @@ import * as assert from 'assert'; -import { getErrorByCode, MediaErrors, TwilioError } from '../../lib/twilio/errors'; +import * as errors from '../../lib/twilio/errors'; -/* tslint:disable-next-line */ -describe('Errors', function() { +describe('Errors', () => { describe('constructor', () => { it('should use message', () => { - const error: TwilioError = new MediaErrors.ConnectionError('foobar'); + const error = new errors.MediaErrors.ConnectionError('foobar'); assert(error instanceof Error); - assertTwilioError(error); - assert(error instanceof MediaErrors.ConnectionError); + assert(error instanceof errors.TwilioError); + assert(error instanceof errors.MediaErrors.ConnectionError); assert.equal(error.code, 53405); assert.equal(error.message, 'ConnectionError (53405): foobar'); assert.equal(error.originalError, undefined); @@ -16,10 +15,10 @@ describe('Errors', function() { it('should use originalError', () => { const originalError = new Error('foobar'); - const error: TwilioError = new MediaErrors.ConnectionError(originalError); + const error = new errors.MediaErrors.ConnectionError(originalError); assert(error instanceof Error); - assertTwilioError(error); - assert(error instanceof MediaErrors.ConnectionError); + assert(error instanceof errors.TwilioError); + assert(error instanceof errors.MediaErrors.ConnectionError); assert.equal(error.code, 53405); assert.equal(error.message, 'ConnectionError (53405): Raised by the Client or Server whenever a media connection fails.'); assert.equal(error.originalError, originalError); @@ -27,20 +26,20 @@ describe('Errors', function() { it('should use both message and originalError', () => { const originalError = new Error('foobar'); - const error: TwilioError = new MediaErrors.ConnectionError('foobar', originalError); + const error = new errors.MediaErrors.ConnectionError('foobar', originalError); assert(error instanceof Error); - assertTwilioError(error); - assert(error instanceof MediaErrors.ConnectionError); + assert(error instanceof errors.TwilioError); + assert(error instanceof errors.MediaErrors.ConnectionError); assert.equal(error.code, 53405); assert.equal(error.message, 'ConnectionError (53405): foobar'); assert.equal(error.originalError, originalError); }); it('should allow no params', () => { - const error: TwilioError = new MediaErrors.ConnectionError(); + const error = new errors.MediaErrors.ConnectionError(); assert(error instanceof Error); - assertTwilioError(error); - assert(error instanceof MediaErrors.ConnectionError); + assert(error instanceof errors.TwilioError); + assert(error instanceof errors.MediaErrors.ConnectionError); assert.equal(error.code, 53405); assert.equal(error.message, 'ConnectionError (53405): Raised by the Client or Server whenever a media connection fails.'); assert.equal(error.originalError, undefined); @@ -49,21 +48,25 @@ describe('Errors', function() { describe('getErrorByCode', () => { it('should throw if code is not found', () => { - assert.throws(() => getErrorByCode(123456)); + assert.throws(() => errors.getErrorByCode(123456)); }); it('should return the TwilioError if code is found', () => { - const twilioError: any = getErrorByCode(53405); - const error: TwilioError = new twilioError(); + const twilioError: any = errors.getErrorByCode(53405); + const error = new twilioError(); assert(error instanceof Error); - assertTwilioError(error); - assert(error instanceof MediaErrors.ConnectionError); + assert(error instanceof errors.TwilioError); + assert(error instanceof errors.MediaErrors.ConnectionError); assert.equal(error.code, 53405); }); }); -}); -// Currently the only way to check an interface at runtime. -function assertTwilioError(error: Error): error is TwilioError { - return true; -} + it('generated error map matches subset of errors in scripts', () => { + const USED_ERRORS: string = require('../../scripts/errors').USED_ERRORS; + for (const errorFullName of USED_ERRORS) { + const [namespace, name] = errorFullName.split('.'); + const errorConstructor = (errors as any)[namespace][name]; + assert(typeof errorConstructor === 'function'); + } + }) +}); From 20f2b445aef634e51aa6fb76bc4912aa049964fc Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Fri, 25 Aug 2023 16:31:32 -0700 Subject: [PATCH 02/15] docs: changelog entry --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 447d7f80..a61cbd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ :warning: **Important**: If you are upgrading to version 2.3.0 or later and have firewall rules or network configuration that blocks any unknown traffic by default, you need to update your configuration to allow connections to the new DNS names and IP addresses. Please refer to this [changelog](#230-january-23-2023) for more details. +2.8.0 (In Progress) +=================== + +Changes +------- + +- Adjusted error reporting from backend services. Signaling errors emitted by the `Device` should now be more descriptive. If your application logic relied on parsing generic error code `31005`, please consider reading through our API documentation and adjusting your application logic to anticipate a wider and more descriptive range of error codes. + + 2.7.1 (August 3, 2023) ====================== From dd5cdff4faa4ec4a7b87a503208eaf985869db70 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Wed, 30 Aug 2023 14:50:12 -0700 Subject: [PATCH 03/15] fix: call hangup handler --- CHANGELOG.md | 10 ++++++++-- lib/twilio/call.ts | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a61cbd19..4cbd8ceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,14 @@ Changes ------- -- Adjusted error reporting from backend services. Signaling errors emitted by the `Device` should now be more descriptive. If your application logic relied on parsing generic error code `31005`, please consider reading through our API documentation and adjusting your application logic to anticipate a wider and more descriptive range of error codes. - +- Enhanced error reporting from backend services. Signaling errors emitted by `Device` and `Call` objects should now be more descriptive. If your application logic relied on parsing generic error code `31005`, please consider reading through our API documentation and adjusting your application logic to anticipate a wider and more descriptive range of error codes. + - Some new error codes included within this release are: + - **ClientErrors** + - NotFound: `31404` + - TemporarilyUnavailable: `31480` + - BusyHere: `31486` + - **SIPServerErrors** + - Decline: `31603` 2.7.1 (August 3, 2023) ====================== diff --git a/lib/twilio/call.ts b/lib/twilio/call.ts index 08003280..24c547d0 100644 --- a/lib/twilio/call.ts +++ b/lib/twilio/call.ts @@ -10,6 +10,8 @@ import Device from './device'; import DialtonePlayer from './dialtonePlayer'; import { GeneralErrors, + getErrorByCode, + hasErrorByCode, InvalidArgumentError, InvalidStateError, MediaErrors, @@ -1204,7 +1206,10 @@ class Call extends EventEmitter { this._log.info('Received HANGUP from gateway'); if (payload.error) { - const error = new GeneralErrors.ConnectionError('Error sent from gateway in HANGUP'); + const code = payload.error.code; + const error = typeof code === 'number' && hasErrorByCode(code) + ? new (getErrorByCode(code))(payload.error.message) + : new GeneralErrors.ConnectionError('Error sent from gateway in HANGUP'); this._log.error('Received an error from the gateway:', error); this.emit('error', error); } From db2a6bc2a24e3f26d0423bae6f286d9a0ae91ef2 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Fri, 8 Sep 2023 13:47:10 -0700 Subject: [PATCH 04/15] docs: update changelog --- CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbd8ceb..2cc8495b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,14 @@ Changes ------- -- Enhanced error reporting from backend services. Signaling errors emitted by `Device` and `Call` objects should now be more descriptive. If your application logic relied on parsing generic error code `31005`, please consider reading through our API documentation and adjusting your application logic to anticipate a wider and more descriptive range of error codes. - - Some new error codes included within this release are: - - **ClientErrors** - - NotFound: `31404` - - TemporarilyUnavailable: `31480` - - BusyHere: `31486` - - **SIPServerErrors** - - Decline: `31603` +- Added new errors emitted by `Device` and `Call` objects. Previously, these errors were all part of a generic error code `31005`. With this new release, the following errors now have their own error codes. Please see this [page](https://www.twilio.com/docs/api/errors) for more details about each error. + - **ClientErrors** + - NotFound: `31404` + - TemporarilyUnavailable: `31480` + - BusyHere: `31486` + - **SIPServerErrors** + - Decline: `31603` +*IMPORTANT: If your application currently relies on listening to the generic error code `31005` when any of the above errors happen, you need to update your error listeners to also listen for the new error codes.* 2.7.1 (August 3, 2023) ====================== From 1fd63a46f00b832ecec7c87f99c6ea30a41ef896 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Thu, 14 Sep 2023 14:40:32 -0700 Subject: [PATCH 05/15] feat: add error feature flag and handling --- lib/twilio/call.ts | 18 ++++++++++---- lib/twilio/device.ts | 49 +++++++++++++++++++++++++++----------- lib/twilio/errors/index.ts | 36 ++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 19 deletions(-) diff --git a/lib/twilio/call.ts b/lib/twilio/call.ts index 24c547d0..cefe41e5 100644 --- a/lib/twilio/call.ts +++ b/lib/twilio/call.ts @@ -10,8 +10,7 @@ import Device from './device'; import DialtonePlayer from './dialtonePlayer'; import { GeneralErrors, - getErrorByCode, - hasErrorByCode, + getErrorByFeatureFlagAndCode, InvalidArgumentError, InvalidStateError, MediaErrors, @@ -249,6 +248,7 @@ class Call extends EventEmitter { */ private _options: Call.Options = { MediaHandler: PeerConnection, + enableImprovedSignalingErrorPrecision: false, offerSdp: null, shouldPlayDisconnect: () => true, voiceEventSidGenerator: generateVoiceEventSid, @@ -1207,9 +1207,15 @@ class Call extends EventEmitter { this._log.info('Received HANGUP from gateway'); if (payload.error) { const code = payload.error.code; - const error = typeof code === 'number' && hasErrorByCode(code) - ? new (getErrorByCode(code))(payload.error.message) - : new GeneralErrors.ConnectionError('Error sent from gateway in HANGUP'); + const errorConstructor = getErrorByFeatureFlagAndCode( + this._options.enableImprovedSignalingErrorPrecision, + code, + ); + const error = typeof errorConstructor !== 'undefined' + ? new errorConstructor(payload.error.message) + : new GeneralErrors.ConnectionError( + 'Error sent from gateway in HANGUP', + ); this._log.error('Received an error from the gateway:', error); this.emit('error', error); } @@ -1836,6 +1842,8 @@ namespace Call { */ dscp?: boolean; + enableImprovedSignalingErrorPrecision: boolean; + /** * Experimental feature. * Force Chrome's ICE agent to use aggressive nomination when selecting a candidate pair. diff --git a/lib/twilio/device.ts b/lib/twilio/device.ts index 38672135..7852e6b8 100644 --- a/lib/twilio/device.ts +++ b/lib/twilio/device.ts @@ -14,8 +14,7 @@ import { AuthorizationErrors, ClientErrors, GeneralErrors, - getErrorByCode, - hasErrorByCode, + getErrorByFeatureFlagAndCode, InvalidArgumentError, InvalidStateError, NotSupportedError, @@ -339,6 +338,7 @@ class Device extends EventEmitter { closeProtection: false, codecPreferences: [Call.Codec.PCMU, Call.Codec.Opus], dscp: true, + enableImprovedSignalingErrorPrecision: false, forceAggressiveIceNomination: false, logLevel: LogLevels.ERROR, maxCallSignalingTimeoutMs: 0, @@ -554,10 +554,15 @@ class Device extends EventEmitter { throw new InvalidStateError('A Call is already active'); } - const activeCall = this._activeCall = await this._makeCall(options.params || { }, { - rtcConfiguration: options.rtcConfiguration, - voiceEventSidGenerator: this._options.voiceEventSidGenerator, - }); + const activeCall = this._activeCall = await this._makeCall( + options.params || { }, + { + enableImprovedSignalingErrorPrecision: + !!this._options.enableImprovedSignalingErrorPrecision, + rtcConfiguration: options.rtcConfiguration, + voiceEventSidGenerator: this._options.voiceEventSidGenerator, + }, + ); // Make sure any incoming calls are ignored this._calls.splice(0).forEach(call => call.ignore()); @@ -1164,8 +1169,14 @@ class Device extends EventEmitter { // Stop trying to register presence after token expires this._stopRegistrationTimer(); twilioError = new AuthorizationErrors.AccessTokenExpired(originalError); - } else if (hasErrorByCode(code)) { - twilioError = new (getErrorByCode(code))(originalError); + } else { + const errorConstructor = getErrorByFeatureFlagAndCode( + !!this._options.enableImprovedSignalingErrorPrecision, + code, + ); + if (typeof errorConstructor !== 'undefined') { + twilioError = new errorConstructor(originalError); + } } } @@ -1198,12 +1209,17 @@ class Device extends EventEmitter { const customParameters = Object.assign({ }, queryToJson(callParameters.Params)); - const call = await this._makeCall(customParameters, { - callParameters, - offerSdp: payload.sdp, - reconnectToken: payload.reconnect, - voiceEventSidGenerator: this._options.voiceEventSidGenerator, - }); + const call = await this._makeCall( + customParameters, + { + callParameters, + enableImprovedSignalingErrorPrecision: + !!this._options.enableImprovedSignalingErrorPrecision, + offerSdp: payload.sdp, + reconnectToken: payload.reconnect, + voiceEventSidGenerator: this._options.voiceEventSidGenerator, + }, + ); this._calls.push(call); @@ -1701,6 +1717,11 @@ namespace Device { */ edge?: string[] | string; + /** + * Granular error codes. + */ + enableImprovedSignalingErrorPrecision?: boolean; + /** * Overrides the native MediaDevices.enumerateDevices API. */ diff --git a/lib/twilio/errors/index.ts b/lib/twilio/errors/index.ts index b2b9df50..6a92c408 100644 --- a/lib/twilio/errors/index.ts +++ b/lib/twilio/errors/index.ts @@ -16,6 +16,42 @@ import { UserMediaErrors, } from './generated'; +/** + * NOTE(mhuynh): Replacing generic error codes with new (more specific) codes, + * is a breaking change. If an error code is found in this set, we only perform + * the transformation if the feature flag is enabled. + * + * With every major version bump, such that we are allowed to introduce breaking + * changes as per semver specification, this array should be cleared. + */ +const FEATURE_FLAG_ERROR_CODES: Set = new Set([ + 31404, + 31480, + 31486, + 31603, +]); +export function getErrorByFeatureFlagAndCode( + enableImprovedSignalingErrorPrecision: boolean, + errorCode: number, +): typeof TwilioError | undefined { + if (typeof errorCode !== 'number') { + return; + } + + if (!hasErrorByCode(errorCode)) { + return; + } + + const shouldTransform = enableImprovedSignalingErrorPrecision + ? true + : !FEATURE_FLAG_ERROR_CODES.has(errorCode); + if (!shouldTransform) { + return; + } + + return getErrorByCode(errorCode); +} + // Application errors that can be avoided by good app logic export class InvalidArgumentError extends Error { constructor(message?: string) { From b0af62b1e050629f4e2233ccc0ab1133a9d33c42 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Thu, 14 Sep 2023 14:41:39 -0700 Subject: [PATCH 06/15] chore: unit tests --- tests/unit/call.ts | 56 ++++++++++++++++++++++++++++++++++++++++---- tests/unit/device.ts | 27 +++++++++++++++++++-- tests/unit/error.ts | 36 +++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/tests/unit/call.ts b/tests/unit/call.ts index a967eaf1..029f4acc 100644 --- a/tests/unit/call.ts +++ b/tests/unit/call.ts @@ -85,6 +85,7 @@ describe('Call', function() { }; options = { + enableImprovedSignalingErrorPrecision: false, MediaHandler, StatsMonitor, voiceEventSidGenerator, @@ -380,7 +381,7 @@ describe('Call', function() { }); it('should result in a `denied` error when `getUserMedia` does not allow the application to access the media', () => { - return new Promise(resolve => { + return new Promise(resolve => { mediaHandler.openWithConstraints = () => { const p = Promise.reject({ code: 0, @@ -1009,7 +1010,7 @@ describe('Call', function() { sinon.assert.calledWith(publisher.info, 'feedback', 'received-none'); }); - Object.values(Call.FeedbackScore).forEach((score: Call.FeedbackScore) => { + Object.values(Call.FeedbackScore).forEach((score: any) => { Object.values(Call.FeedbackIssue).forEach((issue: Call.FeedbackIssue) => { it(`should call publisher.info with feedback received-none when called with ${score} and ${issue}`, () => { conn.postFeedback(score, issue); @@ -1677,6 +1678,53 @@ describe('Call', function() { const rVal = callback.firstCall.args[0]; assert.equal(rVal.code, 31005); }); + + describe('should transform an error if enableImprovedSignalingErrorPrecision is true', () => { + beforeEach(() => { + conn['_options'].enableImprovedSignalingErrorPrecision = true; + }); + + it('should pass the signaling error message', () => { + const cb = sinon.stub(); + conn.on('error', cb); + pstream.emit('hangup', { callsid: 'CA123', error: { + code: 31480, + message: 'foobar', + }}); + const rVal = cb.firstCall.args[0]; + assert.equal(rVal.code, 31480); + assert.equal(rVal.message, 'TemporarilyUnavailable (31480): foobar'); + }) + + it('should default the error message', () => { + const cb = sinon.stub(); + conn.on('error', cb); + pstream.emit('hangup', { callsid: 'CA123', error: { + code: 31480, + }}); + const rVal = cb.firstCall.args[0]; + assert.equal(rVal.code, 31480); + assert.equal( + rVal.message, + 'TemporarilyUnavailable (31480): The callee is currently unavailable.', + ); + }); + }) + + it('should not transform an error if enableImprovedSignalingErrorPrecision is false', () => { + conn['_options'].enableImprovedSignalingErrorPrecision = false; + const cb = sinon.stub(); + conn.on('error', cb); + pstream.emit('hangup', { callsid: 'CA123', error: { + code: 31480, + }}); + const rVal = cb.firstCall.args[0]; + assert.equal(rVal.code, 31005); + assert.equal( + rVal.message, + 'ConnectionError (31005): Error sent from gateway in HANGUP', + ); + }); }); context('when callsid does not match', () => { @@ -2036,7 +2084,7 @@ describe('Call', function() { context('after 10 samples have been emitted', () => { it('should call publisher.postMetrics with the samples', () => { - const samples = []; + const samples: any[] = []; for (let i = 0; i < 10; i++) { const sample = {...sampleData, ...audioData} @@ -2049,7 +2097,7 @@ describe('Call', function() { }); it('should publish correct volume levels', () => { - const samples = []; + const samples: any = []; const dataLength = 10; const convert = (internalValue: any) => (internalValue / 255) * 32767; diff --git a/tests/unit/device.ts b/tests/unit/device.ts index 53379f60..d01fabf9 100644 --- a/tests/unit/device.ts +++ b/tests/unit/device.ts @@ -639,6 +639,29 @@ describe('Device', function() { sinon.assert.notCalled(pstream.register); }); + it('should transform when enableImprovedSignalingErrorPrecision is true', async () => { + device.updateOptions({ enableImprovedSignalingErrorPrecision: true }); + await registerDevice(); + device.emit = sinon.spy(); + pstream.emit('error', { error: { code: 31480 } }); + sinon.assert.calledOnce(device.emit as sinon.SinonSpy); + sinon.assert.calledWith(device.emit as sinon.SinonSpy, 'error'); + const errorObject = (device.emit as sinon.SinonSpy).getCall(0).args[1]; + assert.equal('TemporarilyUnavailable', errorObject.name); + assert.equal(31480, errorObject.code); + }); + + it('should default when enableImprovedSignalingErrorPrecision is false', async () => { + device.updateOptions({ enableImprovedSignalingErrorPrecision: false }); + await registerDevice(); + device.emit = sinon.spy(); + pstream.emit('error', { error: { code: 31480 } }); + sinon.assert.calledOnce(device.emit as sinon.SinonSpy); + sinon.assert.calledWith(device.emit as sinon.SinonSpy, 'error'); + const errorObject = (device.emit as sinon.SinonSpy).getCall(0).args[1]; + console.error(errorObject); + }); + it('should emit Device.error if code is 31005', () => { device.emit = sinon.spy(); pstream.emit('error', { error: { code: 31005 } }); @@ -783,7 +806,7 @@ describe('Device', function() { spyIncomingSound = { play: sinon.spy(), stop: sinon.spy() }; device['_soundcache'].set(Device.SoundName.Incoming, spyIncomingSound); - const incomingPromise = new Promise(resolve => + const incomingPromise = new Promise(resolve => device.once(Device.EventName.Incoming, () => { device.emit = sinon.spy(); device.calls[0].parameters = { }; @@ -1181,7 +1204,7 @@ describe('Device', function() { it('should not create a stream', async () => { const setupSpy = device['_setupStream'] = sinon.spy(device['_setupStream']); device.updateOptions(); - await new Promise(resolve => { + await new Promise(resolve => { sinon.assert.notCalled(setupSpy); resolve(); }); diff --git a/tests/unit/error.ts b/tests/unit/error.ts index ac25c333..061f9010 100644 --- a/tests/unit/error.ts +++ b/tests/unit/error.ts @@ -68,5 +68,39 @@ describe('Errors', () => { const errorConstructor = (errors as any)[namespace][name]; assert(typeof errorConstructor === 'function'); } - }) + }); + + describe('getErrorByFeatureFlagAndCode', () => { + it('should return a constructor when using the feature flag and the error is behind the flag', () => { + const errorConstructor = errors.getErrorByFeatureFlagAndCode(true, 31480); + if (typeof errorConstructor !== 'function') { + throw new Error('error constructor should be defined'); + } + const error = new errorConstructor(); + assert.equal(error.code, 31480); + }); + + it('should return undefined when not using the feature flag and the error is behind the flag', () => { + const errorConstructor = errors.getErrorByFeatureFlagAndCode(false, 31480); + assert.equal(typeof errorConstructor, 'undefined'); + }); + + it('should return a constructor when using the feature flag and the error is not behind the flag', () => { + const errorConstructor = errors.getErrorByFeatureFlagAndCode(true, 31009); + if (typeof errorConstructor !== 'function') { + throw new Error('error constructor should be defined'); + } + const error = new errorConstructor(); + assert.equal(error.code, 31009); + }); + + it('should return a constructor when not using the feature flag and the error is not behind the flag', () => { + const errorConstructor = errors.getErrorByFeatureFlagAndCode(false, 31009); + if (typeof errorConstructor !== 'function') { + throw new Error('error constructor should be defined'); + } + const error = new errorConstructor(); + assert.equal(error.code, 31009); + }); + }); }); From b54816fa7778b4e35b9125e84065d58f0f414606 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Fri, 29 Sep 2023 12:11:08 -0700 Subject: [PATCH 07/15] chore: update voice errors version --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index a73883cb..2b833923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@twilio/voice-sdk", - "version": "2.7.1-dev", + "version": "2.7.2-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@twilio/voice-sdk", - "version": "2.7.1-dev", + "version": "2.7.2-dev", "license": "Apache-2.0", "dependencies": { - "@twilio/voice-errors": "1.3.1", + "@twilio/voice-errors": "1.4.0", "@types/md5": "2.3.2", "events": "3.3.0", "loglevel": "1.6.7", @@ -2131,9 +2131,9 @@ "dev": true }, "node_modules/@twilio/voice-errors": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.3.1.tgz", - "integrity": "sha512-CtozqXquzeUqYkYNus3aOEuS6G007UQK6a31QJYKa28j0tZl8ziwTKVSghj6oeRLlQB2btxiiSQzALMMEh2cug==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.4.0.tgz", + "integrity": "sha512-7BCg9MPz+KQ0JJ6Rl5W3Zw3E+i3Tt77VZw3/2i3Z+IPZITmCOQLu1nKx/0Nlj505Xtfr7eY9Mcern5PfIoBW0w==" }, "node_modules/@types/cookie": { "version": "0.4.1", diff --git a/package.json b/package.json index d8f7818b..ca3b6f9d 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "ws": "7.4.6" }, "dependencies": { - "@twilio/voice-errors": "1.3.1", + "@twilio/voice-errors": "1.4.0", "@types/md5": "2.3.2", "events": "3.3.0", "loglevel": "1.6.7", From f0cbb0eeb62e75e15d499c95b6abf0c6e5266409 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Fri, 29 Sep 2023 12:13:57 -0700 Subject: [PATCH 08/15] feat: add new errors to generation scripts --- lib/twilio/errors/index.ts | 2 ++ scripts/errors.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/twilio/errors/index.ts b/lib/twilio/errors/index.ts index 6a92c408..332be012 100644 --- a/lib/twilio/errors/index.ts +++ b/lib/twilio/errors/index.ts @@ -11,6 +11,7 @@ import { MalformedRequestErrors, MediaErrors, SignalingErrors, + SignatureValidationErrors, SIPServerErrors, TwilioError, UserMediaErrors, @@ -104,6 +105,7 @@ export { MalformedRequestErrors, MediaErrors, SignalingErrors, + SignatureValidationErrors, SIPServerErrors, TwilioError, UserMediaErrors, diff --git a/scripts/errors.js b/scripts/errors.js index f9d18f40..8dce6a7e 100644 --- a/scripts/errors.js +++ b/scripts/errors.js @@ -12,22 +12,37 @@ const USED_ERRORS = [ 'AuthorizationErrors.AccessTokenExpired', 'AuthorizationErrors.AccessTokenInvalid', 'AuthorizationErrors.AuthenticationFailed', + 'AuthorizationErrors.AuthorizationError', + 'AuthorizationErrors.InvalidJWTTokenError', + 'AuthorizationErrors.JWTTokenExpirationTooLongError', + 'AuthorizationErrors.JWTTokenExpiredError', + 'AuthorizationErrors.NoValidAccountError', 'AuthorizationErrors.PayloadSizeExceededError', 'AuthorizationErrors.RateExceededError', 'ClientErrors.BadRequest', 'ClientErrors.BusyHere', 'ClientErrors.NotFound', 'ClientErrors.TemporarilyUnavailable', + 'GeneralErrors.ApplicationNotFoundError', 'GeneralErrors.CallCancelledError', 'GeneralErrors.ConnectionError', + 'GeneralErrors.ConnectionDeclinedError', + 'GeneralErrors.ConnectionTimeoutError', 'GeneralErrors.TransportError', 'GeneralErrors.UnknownError', + 'MalformedRequestErrors.AuthorizationTokenMissingError', + 'MalformedRequestErrors.InvalidBridgeTokenError', + 'MalformedRequestErrors.InvalidClientNameError', 'MalformedRequestErrors.MalformedRequestError', + 'MalformedRequestErrors.MaxParameterLengthExceededError', + 'MalformedRequestErrors.MissingParameterArrayError', + 'MalformedRequestErrors.ReconnectParameterInvalidError', 'MediaErrors.ClientLocalDescFailed', 'MediaErrors.ClientRemoteDescFailed', 'MediaErrors.ConnectionError', 'SignalingErrors.ConnectionDisconnected', 'SignalingErrors.ConnectionError', + 'SignatureValidationErrors.AccessTokenSignatureValidationFailed', 'SIPServerErrors.Decline', 'UserMediaErrors.PermissionDeniedError', 'UserMediaErrors.AcquisitionFailedError', From 1c72025aa1544484671d67e44b37c86ae80208bb Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Fri, 29 Sep 2023 12:15:10 -0700 Subject: [PATCH 09/15] chore: commit generated errors --- lib/twilio/errors/generated.ts | 460 +++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) diff --git a/lib/twilio/errors/generated.ts b/lib/twilio/errors/generated.ts index 57753568..41e44ac5 100644 --- a/lib/twilio/errors/generated.ts +++ b/lib/twilio/errors/generated.ts @@ -100,6 +100,41 @@ export namespace AuthorizationErrors { } } +export namespace SignatureValidationErrors { + export class AccessTokenSignatureValidationFailed extends TwilioError { + causes: string[] = [ + 'The access token has an invalid Account SID, API Key, or API Key Secret.', + ]; + code: number = 31202; + description: string = 'Signature validation failed.'; + explanation: string = 'The provided access token failed signature validation.'; + name: string = 'AccessTokenSignatureValidationFailed'; + solutions: string[] = [ + 'Ensure the Account SID, API Key, and API Key Secret are valid when generating your access token.', + ]; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, SignatureValidationErrors.AccessTokenSignatureValidationFailed.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } +} + export namespace ClientErrors { export class BadRequest extends TwilioError { causes: string[] = []; @@ -285,6 +320,93 @@ export namespace GeneralErrors { } } + export class ApplicationNotFoundError extends TwilioError { + causes: string[] = []; + code: number = 31001; + description: string = 'Application Not Found'; + explanation: string = ''; + name: string = 'ApplicationNotFoundError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, GeneralErrors.ApplicationNotFoundError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class ConnectionDeclinedError extends TwilioError { + causes: string[] = []; + code: number = 31002; + description: string = 'Connection Declined'; + explanation: string = ''; + name: string = 'ConnectionDeclinedError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, GeneralErrors.ConnectionDeclinedError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class ConnectionTimeoutError extends TwilioError { + causes: string[] = []; + code: number = 31003; + description: string = 'Connection Timeout'; + explanation: string = 'The server could not produce a response within a suitable amount of time.'; + name: string = 'ConnectionTimeoutError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, GeneralErrors.ConnectionTimeoutError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + export class ConnectionError extends TwilioError { causes: string[] = []; code: number = 31005; @@ -408,9 +530,303 @@ export namespace MalformedRequestErrors { this.originalError = originalError; } } + + export class MissingParameterArrayError extends TwilioError { + causes: string[] = []; + code: number = 31101; + description: string = 'Missing parameter array in request'; + explanation: string = ''; + name: string = 'MissingParameterArrayError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, MalformedRequestErrors.MissingParameterArrayError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class AuthorizationTokenMissingError extends TwilioError { + causes: string[] = []; + code: number = 31102; + description: string = 'Authorization token missing in request.'; + explanation: string = ''; + name: string = 'AuthorizationTokenMissingError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, MalformedRequestErrors.AuthorizationTokenMissingError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class MaxParameterLengthExceededError extends TwilioError { + causes: string[] = []; + code: number = 31103; + description: string = 'Maximum parameter length has been exceeded.'; + explanation: string = 'Length of parameters cannot exceed MAX_PARAM_LENGTH.'; + name: string = 'MaxParameterLengthExceededError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, MalformedRequestErrors.MaxParameterLengthExceededError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class InvalidBridgeTokenError extends TwilioError { + causes: string[] = []; + code: number = 31104; + description: string = 'Invalid bridge token'; + explanation: string = ''; + name: string = 'InvalidBridgeTokenError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, MalformedRequestErrors.InvalidBridgeTokenError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class InvalidClientNameError extends TwilioError { + causes: string[] = [ + 'Client name contains invalid characters.', + ]; + code: number = 31105; + description: string = 'Invalid client name'; + explanation: string = 'Client name should not contain control, space, delims, or unwise characters.'; + name: string = 'InvalidClientNameError'; + solutions: string[] = [ + 'Make sure that client name does not contain any of the invalid characters.', + ]; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, MalformedRequestErrors.InvalidClientNameError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class ReconnectParameterInvalidError extends TwilioError { + causes: string[] = []; + code: number = 31107; + description: string = 'The reconnect parameter is invalid'; + explanation: string = ''; + name: string = 'ReconnectParameterInvalidError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, MalformedRequestErrors.ReconnectParameterInvalidError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } } export namespace AuthorizationErrors { + export class AuthorizationError extends TwilioError { + causes: string[] = []; + code: number = 31201; + description: string = 'Authorization error'; + explanation: string = 'The request requires user authentication. The server understood the request, but is refusing to fulfill it.'; + name: string = 'AuthorizationError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, AuthorizationErrors.AuthorizationError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class NoValidAccountError extends TwilioError { + causes: string[] = []; + code: number = 31203; + description: string = 'No valid account'; + explanation: string = ''; + name: string = 'NoValidAccountError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, AuthorizationErrors.NoValidAccountError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class InvalidJWTTokenError extends TwilioError { + causes: string[] = []; + code: number = 31204; + description: string = 'Invalid JWT token'; + explanation: string = ''; + name: string = 'InvalidJWTTokenError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, AuthorizationErrors.InvalidJWTTokenError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + + export class JWTTokenExpiredError extends TwilioError { + causes: string[] = []; + code: number = 31205; + description: string = 'JWT token expired'; + explanation: string = ''; + name: string = 'JWTTokenExpiredError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, AuthorizationErrors.JWTTokenExpiredError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + export class RateExceededError extends TwilioError { causes: string[] = [ 'Rate limit exceeded.', @@ -444,6 +860,35 @@ export namespace AuthorizationErrors { } } + export class JWTTokenExpirationTooLongError extends TwilioError { + causes: string[] = []; + code: number = 31207; + description: string = 'JWT token expiration too long'; + explanation: string = ''; + name: string = 'JWTTokenExpirationTooLongError'; + solutions: string[] = []; + + constructor(); + constructor(message: string); + constructor(error: Error | object); + constructor(message: string, error: Error | object); + constructor(messageOrError?: string | Error | object, error?: Error | object) { + super(messageOrError, error); + Object.setPrototypeOf(this, AuthorizationErrors.JWTTokenExpirationTooLongError.prototype); + + const message: string = typeof messageOrError === 'string' + ? messageOrError + : this.explanation; + + const originalError: Error | object | undefined = typeof messageOrError === 'object' + ? messageOrError + : error; + + this.message = `${this.name} (${this.code}): ${message}`; + this.originalError = originalError; + } + } + export class PayloadSizeExceededError extends TwilioError { causes: string[] = [ 'The payload size of Call Message Event exceeds the authorized limit.', @@ -728,17 +1173,32 @@ export const errorsByCode: ReadonlyMap = new Map([ [ 20101, AuthorizationErrors.AccessTokenInvalid ], [ 20104, AuthorizationErrors.AccessTokenExpired ], [ 20151, AuthorizationErrors.AuthenticationFailed ], + [ 31202, SignatureValidationErrors.AccessTokenSignatureValidationFailed ], [ 31400, ClientErrors.BadRequest ], [ 31404, ClientErrors.NotFound ], [ 31480, ClientErrors.TemporarilyUnavailable ], [ 31486, ClientErrors.BusyHere ], [ 31603, SIPServerErrors.Decline ], [ 31000, GeneralErrors.UnknownError ], + [ 31001, GeneralErrors.ApplicationNotFoundError ], + [ 31002, GeneralErrors.ConnectionDeclinedError ], + [ 31003, GeneralErrors.ConnectionTimeoutError ], [ 31005, GeneralErrors.ConnectionError ], [ 31008, GeneralErrors.CallCancelledError ], [ 31009, GeneralErrors.TransportError ], [ 31100, MalformedRequestErrors.MalformedRequestError ], + [ 31101, MalformedRequestErrors.MissingParameterArrayError ], + [ 31102, MalformedRequestErrors.AuthorizationTokenMissingError ], + [ 31103, MalformedRequestErrors.MaxParameterLengthExceededError ], + [ 31104, MalformedRequestErrors.InvalidBridgeTokenError ], + [ 31105, MalformedRequestErrors.InvalidClientNameError ], + [ 31107, MalformedRequestErrors.ReconnectParameterInvalidError ], + [ 31201, AuthorizationErrors.AuthorizationError ], + [ 31203, AuthorizationErrors.NoValidAccountError ], + [ 31204, AuthorizationErrors.InvalidJWTTokenError ], + [ 31205, AuthorizationErrors.JWTTokenExpiredError ], [ 31206, AuthorizationErrors.RateExceededError ], + [ 31207, AuthorizationErrors.JWTTokenExpirationTooLongError ], [ 31209, AuthorizationErrors.PayloadSizeExceededError ], [ 31401, UserMediaErrors.PermissionDeniedError ], [ 31402, UserMediaErrors.AcquisitionFailedError ], From c4c9f550c876186d829b26f3d6c9ed7fb9921f70 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Mon, 2 Oct 2023 16:24:05 -0700 Subject: [PATCH 10/15] fix: add error codes to feature flag set --- lib/twilio/errors/index.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/twilio/errors/index.ts b/lib/twilio/errors/index.ts index 332be012..efa31750 100644 --- a/lib/twilio/errors/index.ts +++ b/lib/twilio/errors/index.ts @@ -26,9 +26,39 @@ import { * changes as per semver specification, this array should be cleared. */ const FEATURE_FLAG_ERROR_CODES: Set = new Set([ + /** + * 310XX Errors + */ + 31001, + 31002, + 31003, + /** + * 311XX Errors + */ + 31101, + 31102, + 31103, + 31104, + 31105, + 31107, + /** + * 312XX Errors + */ + 31201, + 31202, + 31203, + 31204, + 31205, + 31207, + /** + * 314XX Errors + */ 31404, 31480, 31486, + /** + * 316XX Errors + */ 31603, ]); export function getErrorByFeatureFlagAndCode( From 313fc9a8c0a9eb145731b0607e4e0d74e103aa84 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Mon, 2 Oct 2023 19:59:18 -0700 Subject: [PATCH 11/15] docs: update changelog entry --- CHANGELOG.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e8be3b..b91bf3b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,68 @@ New Features ------------ -- Added new errors emitted by `Device` and `Call` objects. Previously, these errors were all part of a generic error code `31005`. With this new release, the following errors now have their own error codes. Please see this [page](https://www.twilio.com/docs/api/errors) for more details about each error. - - **ClientErrors** - - NotFound: `31404` - - TemporarilyUnavailable: `31480` - - BusyHere: `31486` - - **SIPServerErrors** - - Decline: `31603` -*IMPORTANT: If your application currently relies on listening to the generic error code `31005` when any of the above errors happen, you need to update your error listeners to also listen for the new error codes.* +- Added a new feature flag `enableImprovedSignalingErrorPrecision` to enhance the precision of errors emitted by `Device` and `Call` objects. + + ```ts + const token = ...; + const device = new Device(token, { + enableImprovedSignalingErrorPrecision: true, + }); + ``` + + The default value of this option is `false`. + + When this flag is enabled, some errors that would have been described with a generic error code are now described with a more precise error code. With this feature, the following errors now have their own error codes. Please see this [page](https://www.twilio.com/docs/api/errors) for more details about each error. + + - Device Error Changes + + ```ts + const device = new Device(token, { + enableImprovedSignalingErrorPrecision: true, + }); + device.on('error', (deviceError) => { + // the following table describes how deviceError will change with this feature flag + }); + ``` + + | Device Error Name | Device Error Code with Feature Flag Enabled | Device Error Code with Feature Flag Disabled | + | --- | --- | --- | + | `GeneralErrors.ApplicationNotFoundError` | `31001` | `53000` | + | `GeneralErrors.ConnectionDeclinedError` | `31002` | `53000` | + | `GeneralErrors.ConnectionTimeoutError` | `31003` | `53000` | + | `MalformedRequestErrors.MissingParameterArrayError` | `31101` | `53000` | + | `MalformedRequestErrors.AuthorizationTokenMissingError` | `31102` | `53000` | + | `MalformedRequestErrors.MaxParameterLengthExceededError` | `31103` | `53000` | + | `MalformedRequestErrors.InvalidBridgeTokenError` | `31104` | `53000` | + | `MalformedRequestErrors.InvalidClientNameError` | `31105` | `53000` | + | `MalformedRequestErrors.ReconnectParameterInvalidError` | `31107` | `53000` | + | `SignatureValidationErrors.AccessTokenSignatureValidationFailed` | `31202` | `53000` | + | `AuthorizationErrors.NoValidAccountError` | `31203` | `53000` | + | `AuthorizationErrors.JWTTokenExpirationTooLongError` | `31207` | `53000` | + | `ClientErrors.NotFound` | `31404` | `53000` | + | `ClientErrors.TemporarilyUnavilable` | `31480` | `53000` | + | `ClientErrors.BusyHere` | `31486` | `53000` | + | `SIPServerErrors.Decline` | `31603` | `53000` | + + - Call Error Changes + + ```ts + const device = new Device(token, { + enableImprovedSignalingErrorPrecision: true, + }); + const call = device.connect(...); + call.on('error', (callError) => { + // the following table describes how callError will change with this feature flag + }); + ``` + + | Call Error Name | Call Error Code with Feature Flag Enabled | Call Error Code with Feature Flag Disabled | + | --- | --- | --- | + | `GeneralErrors.ConnectionDeclinedError` | `31002` | `31005` | + | `AuthorizationErrors.InvalidJWTTokenError` | `31204` | `31005` | + | `AuthorizationErrors.JWTTokenExpiredError` | `31205` | `31005` | + + _**IMPORTANT:** If your application logic currently relies on listening to the generic error code `53000` or `31005`, and you opt into enabling the feature flag, then your applicaton logic needs to be updated to anticipate the new error code when any of the above errors happen!_ 2.7.2 (September 21, 2023) ========================= From ebc13d174ac1ff3175ca95e0c5066e1d1f4c6a5b Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Mon, 2 Oct 2023 20:14:29 -0700 Subject: [PATCH 12/15] docs: update docstring for option --- lib/twilio/device.ts | 62 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/twilio/device.ts b/lib/twilio/device.ts index 8dd299ba..8f760fed 100644 --- a/lib/twilio/device.ts +++ b/lib/twilio/device.ts @@ -1734,7 +1734,67 @@ namespace Device { edge?: string[] | string; /** - * Granular error codes. + * Enhance the precision of errors emitted by `Device` and `Call` objects. + * + * The default value of this option is `false`. + * + * When this flag is enabled, some errors that would have been described + * with a generic error code, namely `53000` and `31005`, are now described + * with a more precise error code. With this feature, the following errors + * now have their own error codes. Please see this + * [page](https://www.twilio.com/docs/api/errors) for more details about + * each error. + * + * - Device Error Changes + * + * @example + * ```ts + * const device = new Device(token, { + * enableImprovedSignalingErrorPrecision: true, + * }); + * device.on('error', (deviceError) => { + * // the following table describes how deviceError will change with this feature flag + * }); + * ``` + * + * | Device Error Name | Device Error Code with Feature Flag Enabled | Device Error Code with Feature Flag Disabled | + * | --- | --- | --- | + * | `GeneralErrors.ApplicationNotFoundError` | `31001` | `53000` | + * | `GeneralErrors.ConnectionDeclinedError` | `31002` | `53000` | + * | `GeneralErrors.ConnectionTimeoutError` | `31003` | `53000` | + * | `MalformedRequestErrors.MissingParameterArrayError` | `31101` | `53000` | + * | `MalformedRequestErrors.AuthorizationTokenMissingError` | `31102` | `53000` | + * | `MalformedRequestErrors.MaxParameterLengthExceededError` | `31103` | `53000` | + * | `MalformedRequestErrors.InvalidBridgeTokenError` | `31104` | `53000` | + * | `MalformedRequestErrors.InvalidClientNameError` | `31105` | `53000` | + * | `MalformedRequestErrors.ReconnectParameterInvalidError` | `31107` | `53000` | + * | `SignatureValidationErrors.AccessTokenSignatureValidationFailed` | `31202` | `53000` | + * | `AuthorizationErrors.NoValidAccountError` | `31203` | `53000` | + * | `AuthorizationErrors.JWTTokenExpirationTooLongError` | `31207` | `53000` | + * | `ClientErrors.NotFound` | `31404` | `53000` | + * | `ClientErrors.TemporarilyUnavilable` | `31480` | `53000` | + * | `ClientErrors.BusyHere` | `31486` | `53000` | + * | `SIPServerErrors.Decline` | `31603` | `53000` | + * + * - Call Error Changes + * + * @example + * ```ts + * const device = new Device(token, { + * enableImprovedSignalingErrorPrecision: true, + * }); + * const call = device.connect(...); + * call.on('error', (callError) => { + * // the following table describes how callError will change with this feature flag + * }); + * ``` + * + * | Call Error Name | Call Error Code with Feature Flag Enabled | Call Error Code with Feature Flag Disabled | + * | --- | --- | --- | + * | `GeneralErrors.ConnectionDeclinedError` | `31002` | `31005` | + * | `AuthorizationErrors.InvalidJWTTokenError` | `31204` | `31005` | + * | `AuthorizationErrors.JWTTokenExpiredError` | `31205` | `31005` | + * */ enableImprovedSignalingErrorPrecision?: boolean; From 428c2de344789c55f83edfaee7298fd050b06f31 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Tue, 3 Oct 2023 13:22:18 -0700 Subject: [PATCH 13/15] docs: less enthusiasm --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b91bf3b5..18367cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ New Features | `AuthorizationErrors.InvalidJWTTokenError` | `31204` | `31005` | | `AuthorizationErrors.JWTTokenExpiredError` | `31205` | `31005` | - _**IMPORTANT:** If your application logic currently relies on listening to the generic error code `53000` or `31005`, and you opt into enabling the feature flag, then your applicaton logic needs to be updated to anticipate the new error code when any of the above errors happen!_ + _**IMPORTANT:** If your application logic currently relies on listening to the generic error code `53000` or `31005`, and you opt into enabling the feature flag, then your applicaton logic needs to be updated to anticipate the new error code when any of the above errors happen._ 2.7.2 (September 21, 2023) ========================= From c893d8d5d284dc1c89db7e27f4f79e0dab16c0e6 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Tue, 3 Oct 2023 13:32:40 -0700 Subject: [PATCH 14/15] fix: rename variables and functions --- lib/twilio/call.ts | 4 ++-- lib/twilio/device.ts | 4 ++-- lib/twilio/errors/index.ts | 8 +++++--- tests/unit/error.ts | 8 ++++---- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/twilio/call.ts b/lib/twilio/call.ts index 5c7ff8e1..45cb9a0d 100644 --- a/lib/twilio/call.ts +++ b/lib/twilio/call.ts @@ -10,7 +10,7 @@ import Device from './device'; import DialtonePlayer from './dialtonePlayer'; import { GeneralErrors, - getErrorByFeatureFlagAndCode, + getPreciseSignalingErrorByCode, InvalidArgumentError, InvalidStateError, MediaErrors, @@ -1211,7 +1211,7 @@ class Call extends EventEmitter { this._log.info('Received HANGUP from gateway'); if (payload.error) { const code = payload.error.code; - const errorConstructor = getErrorByFeatureFlagAndCode( + const errorConstructor = getPreciseSignalingErrorByCode( this._options.enableImprovedSignalingErrorPrecision, code, ); diff --git a/lib/twilio/device.ts b/lib/twilio/device.ts index 8f760fed..131348ca 100644 --- a/lib/twilio/device.ts +++ b/lib/twilio/device.ts @@ -14,7 +14,7 @@ import { AuthorizationErrors, ClientErrors, GeneralErrors, - getErrorByFeatureFlagAndCode, + getPreciseSignalingErrorByCode, InvalidArgumentError, InvalidStateError, NotSupportedError, @@ -1181,7 +1181,7 @@ class Device extends EventEmitter { this._stopRegistrationTimer(); twilioError = new AuthorizationErrors.AccessTokenExpired(originalError); } else { - const errorConstructor = getErrorByFeatureFlagAndCode( + const errorConstructor = getPreciseSignalingErrorByCode( !!this._options.enableImprovedSignalingErrorPrecision, code, ); diff --git a/lib/twilio/errors/index.ts b/lib/twilio/errors/index.ts index efa31750..b29deb5f 100644 --- a/lib/twilio/errors/index.ts +++ b/lib/twilio/errors/index.ts @@ -24,8 +24,10 @@ import { * * With every major version bump, such that we are allowed to introduce breaking * changes as per semver specification, this array should be cleared. + * + * TODO: [VBLOCKS-2295] Remove this in 3.x */ -const FEATURE_FLAG_ERROR_CODES: Set = new Set([ +const PRECISE_SIGNALING_ERROR_CODES: Set = new Set([ /** * 310XX Errors */ @@ -61,7 +63,7 @@ const FEATURE_FLAG_ERROR_CODES: Set = new Set([ */ 31603, ]); -export function getErrorByFeatureFlagAndCode( +export function getPreciseSignalingErrorByCode( enableImprovedSignalingErrorPrecision: boolean, errorCode: number, ): typeof TwilioError | undefined { @@ -75,7 +77,7 @@ export function getErrorByFeatureFlagAndCode( const shouldTransform = enableImprovedSignalingErrorPrecision ? true - : !FEATURE_FLAG_ERROR_CODES.has(errorCode); + : !PRECISE_SIGNALING_ERROR_CODES.has(errorCode); if (!shouldTransform) { return; } diff --git a/tests/unit/error.ts b/tests/unit/error.ts index 061f9010..38121605 100644 --- a/tests/unit/error.ts +++ b/tests/unit/error.ts @@ -72,7 +72,7 @@ describe('Errors', () => { describe('getErrorByFeatureFlagAndCode', () => { it('should return a constructor when using the feature flag and the error is behind the flag', () => { - const errorConstructor = errors.getErrorByFeatureFlagAndCode(true, 31480); + const errorConstructor = errors.getPreciseSignalingErrorByCode(true, 31480); if (typeof errorConstructor !== 'function') { throw new Error('error constructor should be defined'); } @@ -81,12 +81,12 @@ describe('Errors', () => { }); it('should return undefined when not using the feature flag and the error is behind the flag', () => { - const errorConstructor = errors.getErrorByFeatureFlagAndCode(false, 31480); + const errorConstructor = errors.getPreciseSignalingErrorByCode(false, 31480); assert.equal(typeof errorConstructor, 'undefined'); }); it('should return a constructor when using the feature flag and the error is not behind the flag', () => { - const errorConstructor = errors.getErrorByFeatureFlagAndCode(true, 31009); + const errorConstructor = errors.getPreciseSignalingErrorByCode(true, 31009); if (typeof errorConstructor !== 'function') { throw new Error('error constructor should be defined'); } @@ -95,7 +95,7 @@ describe('Errors', () => { }); it('should return a constructor when not using the feature flag and the error is not behind the flag', () => { - const errorConstructor = errors.getErrorByFeatureFlagAndCode(false, 31009); + const errorConstructor = errors.getPreciseSignalingErrorByCode(false, 31009); if (typeof errorConstructor !== 'function') { throw new Error('error constructor should be defined'); } From ff5c5fa3e74abf5bbed2a3aabcb91af94ee38381 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Tue, 3 Oct 2023 14:00:19 -0700 Subject: [PATCH 15/15] fix: rename test case --- tests/unit/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/error.ts b/tests/unit/error.ts index 38121605..197c9e47 100644 --- a/tests/unit/error.ts +++ b/tests/unit/error.ts @@ -70,7 +70,7 @@ describe('Errors', () => { } }); - describe('getErrorByFeatureFlagAndCode', () => { + describe('getPreciseSignalingErrorByCode', () => { it('should return a constructor when using the feature flag and the error is behind the flag', () => { const errorConstructor = errors.getPreciseSignalingErrorByCode(true, 31480); if (typeof errorConstructor !== 'function') {