From a17b0af1a2ae112b7e96da86a8761d5c15d616cf Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 22 Aug 2023 18:54:30 -0400 Subject: [PATCH] feat(NODE-5484)!: mark MongoError for internal use and remove Node14 cause assignment logic (#3800) Co-authored-by: Bailey Pearson --- etc/notes/errors.md | 53 ++- src/bulk/common.ts | 12 +- src/client-side-encryption/auto_encrypter.ts | 3 +- src/client-side-encryption/errors.ts | 63 ++- src/cmap/connect.ts | 2 +- src/cmap/errors.ts | 46 +- src/error.ts | 394 +++++++++++++++++- src/sdam/monitor.ts | 4 +- .../client_side_encryption.prose.test.js | 24 +- test/integration/node-specific/errors.test.ts | 39 +- .../client-side-encryption/errors.test.ts | 41 ++ test/unit/error.test.ts | 55 ++- 12 files changed, 621 insertions(+), 115 deletions(-) create mode 100644 test/unit/client-side-encryption/errors.test.ts diff --git a/etc/notes/errors.md b/etc/notes/errors.md index 03c47e7373..d0f8e6b6e9 100644 --- a/etc/notes/errors.md +++ b/etc/notes/errors.md @@ -18,6 +18,7 @@ - [`MongoNetworkError`](#MongoNetworkError) - [`MongoServerError`](#MongoServerError) - [`MongoSystemError`](#MongoSystemError) + - [`MongoCryptError`](#MongoCryptError) - [Test Plan](#Test-Plan) - [`MongoAPIError`](#MongoAPIError-1) - [`MongoInvalidArgumentError`](#MongoInvalidArgumentError-1) @@ -32,8 +33,8 @@ ## Errors All errors are derived from the `MongoError` class which should **never** be instantiated. -There are four main error classes which stem from `MongoError`: `MongoDriverError`, -`MongoNetworkError`, `MongoServerError`, and `MongoSystemError`. +There are five main error classes which stem from `MongoError`: `MongoDriverError`, +`MongoNetworkError`, `MongoServerError`, `MongoCryptError` and `MongoSystemError`. ### `MongoError` @@ -41,28 +42,18 @@ The base class from which all errors in the Node driver subclass. `MongoError` should **never** be be directly instantiated. ```mermaid + graph TD - MongoError --- MongoDriverError - MongoError --- MongoNetworkError - MongoError --- MongoServerError - MongoError --- MongoSystemError - MongoDriverError --- MongoAPIError - MongoDriverError --- MongoRuntimeError - -linkStyle 0 stroke:#116149 -linkStyle 1 stroke:#116149 -linkStyle 2 stroke:#116149 -linkStyle 3 stroke:#116149 -linkStyle 4 stroke:#116149 -linkStyle 5 stroke:#116149 - -style MongoError fill:#13aa52,stroke:#21313c,color:#FAFBFC -style MongoSystemError fill:#13aa52,stroke:#21313c,color:#FAFBFC -style MongoNetworkError fill:#13aa52,stroke:#21313c,color:#FAFBFC -style MongoServerError fill:#13aa52,stroke:#21313c,color:#FAFBFC -style MongoDriverError fill:#13aa52,stroke:#21313c,color:#FAFBFC -style MongoAPIError fill:#13aa52,stroke:#21313c,color:#FAFBFC -style MongoRuntimeError fill:#13aa52,stroke:#21313c,color:#FAFBFC + MongoError:::node --- MongoDriverError + MongoError:::node --- MongoNetworkError + MongoError:::node --- MongoServerError + MongoError:::node --- MongoSystemError + MongoError:::node --- MongoCryptError + MongoDriverError:::node --- MongoAPIError + MongoDriverError:::node --- MongoRuntimeError + +linkStyle 0,1,2,3,4,5,6 stroke:#116149 +classDef node fill:#13aa52,stroke:#21313c,color:#FAFBFC ``` Children of `MongoError` include: @@ -71,6 +62,7 @@ Children of `MongoError` include: - [`MongoNetworkError`](#MongoNetworkError) - [`MongoServerError`](#MongoServerError) - [`MongoSystemError`](#MongoSystemError) +- [`MongoCryptError`](#MongoCryptError) ### `MongoDriverError` @@ -144,6 +136,21 @@ These are errors which originate from faulty environment setup. - #### MongoServerSelectionError - Thrown when the driver fails to select a server to complete an operation +### `MongoCryptError` + +These are errors thrown from the driver's client side encryption logic. + +- #### MongoCryptInvalidArgumentError + - Thrown when an invalid argument has been provided to an encryption API +- #### MongoCryptInvalidCreateDataKeyError + - Thrown when the driver fails to create data keys for an encrypted collection +- #### MongoCryptInvalidCreateEncryptedCollectionError + - Thrown when the driver fails to create an encrypted collection +- #### MongoCryptInvalidCreateAzureKMSRequestError + - Thrown when the driver encounters an error when fetching Azure KMS credentials +- #### MongoCryptKMSRequestNetworkTimeoutError + - Thrown when the HTTP request to the IDMS server times out when fetching Azure KMS credentials + ## Test Plan The test plan consists of a series of prose tests. diff --git a/src/bulk/common.ts b/src/bulk/common.ts index c9ab3bbcd8..054a2e9e3b 100644 --- a/src/bulk/common.ts +++ b/src/bulk/common.ts @@ -598,7 +598,17 @@ export class MongoBulkWriteError extends MongoServerError { writeErrors: OneOrMore = []; err?: WriteConcernError; - /** Creates a new MongoBulkWriteError */ + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor( error: | { message: string; code: number; writeErrors?: WriteError[] } diff --git a/src/client-side-encryption/auto_encrypter.ts b/src/client-side-encryption/auto_encrypter.ts index 6de294b63a..d6eb0826c8 100644 --- a/src/client-side-encryption/auto_encrypter.ts +++ b/src/client-side-encryption/auto_encrypter.ts @@ -422,7 +422,8 @@ export class AutoEncrypter { ) { callback( new MongoRuntimeError( - 'Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn' + 'Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn', + { cause: err } ) ); return; diff --git a/src/client-side-encryption/errors.ts b/src/client-side-encryption/errors.ts index 9df2b8beb4..7ab70748a8 100644 --- a/src/client-side-encryption/errors.ts +++ b/src/client-side-encryption/errors.ts @@ -1,11 +1,22 @@ import { type Document } from '../bson'; +import { MongoError } from '../error'; /** * @public * An error indicating that something went wrong specifically with MongoDB Client Encryption */ -export class MongoCryptError extends Error { - /** @internal */ +export class MongoCryptError extends MongoError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string, options: { cause?: Error } = {}) { super(message, options); } @@ -21,7 +32,17 @@ export class MongoCryptError extends Error { * An error indicating an invalid argument was provided to an encryption API. */ export class MongoCryptInvalidArgumentError extends MongoCryptError { - /** @internal */ + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -36,7 +57,17 @@ export class MongoCryptInvalidArgumentError extends MongoCryptError { */ export class MongoCryptCreateDataKeyError extends MongoCryptError { encryptedFields: Document; - /** @internal */ + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(encryptedFields: Document, { cause }: { cause: Error }) { super(`Unable to complete creating data keys: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; @@ -53,7 +84,17 @@ export class MongoCryptCreateDataKeyError extends MongoCryptError { */ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { encryptedFields: Document; - /** @internal */ + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(encryptedFields: Document, { cause }: { cause: Error }) { super(`Unable to create collection: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; @@ -71,7 +112,17 @@ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { export class MongoCryptAzureKMSRequestError extends MongoCryptError { /** The body of the http response that failed, if present. */ body?: Document; - /** @internal */ + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string, body?: Document) { super(message); this.body = body; diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 0ea49e939c..3cf6db7447 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -502,7 +502,7 @@ function makeSocks5Connection(options: MakeConnectionOptions, callback: Callback function connectionFailureError(type: ErrorHandlerEventName, err: Error) { switch (type) { case 'error': - return new MongoNetworkError(err); + return new MongoNetworkError(MongoError.buildErrorMessage(err), { cause: err }); case 'timeout': return new MongoNetworkTimeoutError('connection timed out'); case 'close': diff --git a/src/cmap/errors.ts b/src/cmap/errors.ts index f6d2ed5888..af550508e8 100644 --- a/src/cmap/errors.ts +++ b/src/cmap/errors.ts @@ -9,6 +9,17 @@ export class PoolClosedError extends MongoDriverError { /** The address of the connection pool */ address: string; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(pool: ConnectionPool) { super('Attempted to check out a connection from closed connection pool'); this.address = pool.address; @@ -27,11 +38,22 @@ export class PoolClearedError extends MongoNetworkError { /** The address of the connection pool */ address: string; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(pool: ConnectionPool, message?: string) { const errorMessage = message ? message : `Connection pool for ${pool.address} was cleared because another operation failed with: "${pool.serverError?.message}"`; - super(errorMessage); + super(errorMessage, pool.serverError ? { cause: pool.serverError } : undefined); this.address = pool.address; this.addErrorLabel(MongoErrorLabel.RetryableWriteError); @@ -47,6 +69,17 @@ export class PoolClearedError extends MongoNetworkError { * @category Error */ export class PoolClearedOnNetworkError extends PoolClearedError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(pool: ConnectionPool) { super(pool, `Connection to ${pool.address} interrupted due to server monitor timeout`); } @@ -64,6 +97,17 @@ export class WaitQueueTimeoutError extends MongoDriverError { /** The address of the connection pool */ address: string; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string, address: string) { super(message); this.address = address; diff --git a/src/error.ts b/src/error.ts index 8c9f495626..11203a9a9a 100644 --- a/src/error.ts +++ b/src/error.ts @@ -130,21 +130,26 @@ export class MongoError extends Error { code?: number | string; topologyVersion?: TopologyVersion; connectionGeneration?: number; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - cause?: Error; // depending on the node version, this may or may not exist on the base - - constructor(message: string | Error) { - super(MongoError.buildErrorMessage(message)); - if (message instanceof Error) { - this.cause = message; - } + override cause?: Error; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor(message: string, options?: { cause?: Error }) { + super(message, options); this[kErrorLabels] = new Set(); } /** @internal */ - private static buildErrorMessage(e: Error | string): string { + static buildErrorMessage(e: Error | string): string { if (typeof e === 'string') { return e; } @@ -198,6 +203,17 @@ export class MongoServerError extends MongoError { ok?: number; [key: string]: any; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: ErrorDescription) { super(message.message || message.errmsg || message.$err || 'n/a'); if (message.errorLabels) { @@ -222,8 +238,19 @@ export class MongoServerError extends MongoError { * @category Error */ export class MongoDriverError extends MongoError { - constructor(message: string) { - super(message); + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor(message: string, options?: { cause?: Error }) { + super(message, options); } override get name(): string { @@ -242,8 +269,19 @@ export class MongoDriverError extends MongoError { */ export class MongoAPIError extends MongoDriverError { - constructor(message: string) { - super(message); + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor(message: string, options?: { cause?: Error }) { + super(message, options); } override get name(): string { @@ -262,8 +300,19 @@ export class MongoAPIError extends MongoDriverError { * @category Error */ export class MongoRuntimeError extends MongoDriverError { - constructor(message: string) { - super(message); + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor(message: string, options?: { cause?: Error }) { + super(message, options); } override get name(): string { @@ -279,6 +328,17 @@ export class MongoRuntimeError extends MongoDriverError { * @category Error */ export class MongoBatchReExecutionError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message = 'This batch has already been executed, create new batch to execute') { super(message); } @@ -296,6 +356,17 @@ export class MongoBatchReExecutionError extends MongoAPIError { * @category Error */ export class MongoDecompressionError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -313,6 +384,17 @@ export class MongoDecompressionError extends MongoRuntimeError { * @category Error */ export class MongoNotConnectedError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -330,6 +412,17 @@ export class MongoNotConnectedError extends MongoAPIError { * @category Error */ export class MongoTransactionError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -347,6 +440,17 @@ export class MongoTransactionError extends MongoAPIError { * @category Error */ export class MongoExpiredSessionError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message = 'Cannot use a session that has ended') { super(message); } @@ -364,6 +468,17 @@ export class MongoExpiredSessionError extends MongoAPIError { * @category Error */ export class MongoKerberosError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -381,6 +496,17 @@ export class MongoKerberosError extends MongoRuntimeError { * @category Error */ export class MongoAWSError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -398,6 +524,17 @@ export class MongoAWSError extends MongoRuntimeError { * @category Error */ export class MongoAzureError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -414,6 +551,17 @@ export class MongoAzureError extends MongoRuntimeError { * @category Error */ export class MongoChangeStreamError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -430,6 +578,17 @@ export class MongoChangeStreamError extends MongoRuntimeError { * @category Error */ export class MongoTailableCursorError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message = 'Tailable cursor does not support this operation') { super(message); } @@ -445,6 +604,17 @@ export class MongoTailableCursorError extends MongoAPIError { * @category Error */ export class MongoGridFSStreamError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -462,6 +632,17 @@ export class MongoGridFSStreamError extends MongoRuntimeError { * @category Error */ export class MongoGridFSChunkError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -488,6 +669,17 @@ export class MongoGridFSChunkError extends MongoRuntimeError { * @category Error */ export class MongoUnexpectedServerResponseError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -505,6 +697,17 @@ export class MongoUnexpectedServerResponseError extends MongoRuntimeError { * @category Error */ export class MongoCursorInUseError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message = 'Cursor is already initialized') { super(message); } @@ -522,6 +725,17 @@ export class MongoCursorInUseError extends MongoAPIError { * @category Error */ export class MongoServerClosedError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message = 'Server is closed') { super(message); } @@ -538,6 +752,17 @@ export class MongoServerClosedError extends MongoAPIError { * @category Error */ export class MongoCursorExhaustedError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message?: string) { super(message || 'Cursor is exhausted'); } @@ -555,6 +780,17 @@ export class MongoCursorExhaustedError extends MongoAPIError { * @category Error */ export class MongoTopologyClosedError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message = 'Topology is closed') { super(message); } @@ -573,7 +809,8 @@ export function isNetworkErrorBeforeHandshake(err: MongoNetworkError): boolean { /** @public */ export interface MongoNetworkErrorOptions { /** Indicates the timeout happened before a connection handshake completed */ - beforeHandshake: boolean; + beforeHandshake?: boolean; + cause?: Error; } /** @@ -585,8 +822,19 @@ export class MongoNetworkError extends MongoError { /** @internal */ [kBeforeHandshake]?: boolean; - constructor(message: string | Error, options?: MongoNetworkErrorOptions) { - super(message); + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor(message: string, options?: MongoNetworkErrorOptions) { + super(message, { cause: options?.cause }); if (options && typeof options.beforeHandshake === 'boolean') { this[kBeforeHandshake] = options.beforeHandshake; @@ -607,6 +855,17 @@ export class MongoNetworkError extends MongoError { * mongodb-client-encryption has a dependency on this error with an instanceof check */ export class MongoNetworkTimeoutError extends MongoNetworkError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string, options?: MongoNetworkErrorOptions) { super(message, options); } @@ -622,6 +881,17 @@ export class MongoNetworkTimeoutError extends MongoNetworkError { * @category Error */ export class MongoParseError extends MongoDriverError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -640,6 +910,17 @@ export class MongoParseError extends MongoDriverError { * @category Error */ export class MongoInvalidArgumentError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -658,6 +939,17 @@ export class MongoInvalidArgumentError extends MongoAPIError { * @category Error */ export class MongoCompatibilityError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -676,6 +968,17 @@ export class MongoCompatibilityError extends MongoAPIError { * @category Error */ export class MongoMissingCredentialsError extends MongoAPIError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string) { super(message); } @@ -692,9 +995,19 @@ export class MongoMissingCredentialsError extends MongoAPIError { * @category Error */ export class MongoMissingDependencyError extends MongoAPIError { - constructor(message: string, { cause }: { cause?: Error } = {}) { - super(message); - if (cause) this.cause = cause; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor(message: string, options: { cause?: Error } = {}) { + super(message, options); } override get name(): string { @@ -710,9 +1023,22 @@ export class MongoSystemError extends MongoError { /** An optional reason context, such as an error saved during flow of monitoring and selecting servers */ reason?: TopologyDescription; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string, reason: TopologyDescription) { if (reason && reason.error) { - super(reason.error.message || reason.error); + super(MongoError.buildErrorMessage(reason.error.message || reason.error), { + cause: reason.error + }); } else { super(message); } @@ -735,6 +1061,17 @@ export class MongoSystemError extends MongoError { * @category Error */ export class MongoServerSelectionError extends MongoSystemError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: string, reason: TopologyDescription) { super(message, reason); } @@ -766,6 +1103,17 @@ export class MongoWriteConcernError extends MongoServerError { /** The result document (provided if ok: 1) */ result?: Document; + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ constructor(message: ErrorDescription, result?: Document) { if (result && Array.isArray(result.errorLabels)) { message.errorLabels = result.errorLabels; diff --git a/src/sdam/monitor.ts b/src/sdam/monitor.ts index eec2c30ed7..bd5702b4af 100644 --- a/src/sdam/monitor.ts +++ b/src/sdam/monitor.ts @@ -220,7 +220,9 @@ function checkServer(monitor: Monitor, callback: Callback) { new ServerHeartbeatFailedEvent(monitor.address, calculateDurationInMs(start), err) ); - const error = !(err instanceof MongoError) ? new MongoError(err) : err; + const error = !(err instanceof MongoError) + ? new MongoError(MongoError.buildErrorMessage(err), { cause: err }) + : err; error.addErrorLabel(MongoErrorLabel.ResetPool); if (error instanceof MongoNetworkTimeoutError) { error.addErrorLabel(MongoErrorLabel.InterruptInUseConnections); diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.test.js index 2b1bf98a19..b25e9d3545 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.prose.test.js @@ -1171,9 +1171,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { .insertOne({ encrypted: 'test' }) .catch(e => e); - expect(insertError) - .to.be.instanceOf(Error) - .to.have.property('name', 'MongoServerSelectionError'); + expect(insertError).to.be.instanceOf(MongoServerSelectionError); // TODO(NODE-5296): check error.message once AggregateErrors are handled correctly expect(insertError, 'Error must contain ECONNREFUSED').to.satisfy( @@ -1181,8 +1179,6 @@ describe('Client Side Encryption Prose Tests', metadata, function () { /ECONNREFUSED/.test(error.message) || !!error.cause?.cause?.errors?.every(e => e.code === 'ECONNREFUSED') ); - - expect(insertError).to.be.instanceOf(MongoServerSelectionError); }); }); @@ -1264,16 +1260,14 @@ describe('Client Side Encryption Prose Tests', metadata, function () { const error = await client.connect().catch(e => e); // TODO(NODE-5296): check error.message once AggregateErrors are handled correctly - expect( - error, - 'Error MUST be a MongoServerSelectionError error that contains ECONNREFUSED information' - ) - .to.be.instanceOf(MongoServerSelectionError) - .that.satisfies( - error => - /ECONNREFUSED/.test(error.message) || - !!error.cause?.cause?.errors?.every(e => e.code === 'ECONNREFUSED') - ); + expect(error, 'Error MUST be a MongoServerSelectionError error').to.be.instanceOf( + MongoServerSelectionError + ); + expect(error, 'Error MUST contain ECONNREFUSED information').to.satisfy( + error => + /ECONNREFUSED/.test(error.message) || + !!error.cause?.cause?.errors?.every(e => e.code === 'ECONNREFUSED') + ); }); }); diff --git a/test/integration/node-specific/errors.test.ts b/test/integration/node-specific/errors.test.ts index 5011297046..c0890083eb 100644 --- a/test/integration/node-specific/errors.test.ts +++ b/test/integration/node-specific/errors.test.ts @@ -1,45 +1,8 @@ import { expect } from 'chai'; -import { MongoClient, MongoError, MongoServerSelectionError } from '../../mongodb'; +import { MongoClient, MongoServerSelectionError } from '../../mongodb'; describe('Error (Integration)', function () { - describe('AggregateErrors', function () { - for (const { errors, message } of [ - { - errors: [], - message: - 'AggregateError has an empty errors array. Please check the `cause` property for more information.' - }, - { errors: [new Error('message 1')], message: 'message 1' }, - { - errors: [new Error('message 1'), new Error('message 2')], - message: 'message 1, message 2' - } - ]) { - it(`constructs the message properly with an array of ${errors.length} errors`, () => { - const error = new AggregateError(errors); - const mongoError = new MongoError(error); - - expect(mongoError.message).to.equal(message); - }); - } - - context('when the message on the AggregateError is non-empty', () => { - it(`uses the AggregateError's message`, () => { - const error = new AggregateError([new Error('non-empty')]); - error.message = 'custom error message'; - const mongoError = new MongoError(error); - expect(mongoError.message).to.equal('custom error message'); - }); - }); - - it('sets the AggregateError to the cause property', () => { - const error = new AggregateError([new Error('error 1')]); - const mongoError = new MongoError(error); - expect(mongoError.cause).to.equal(error); - }); - }); - it('NODE-5296: handles aggregate errors from dns lookup', async function () { const error = await MongoClient.connect('mongodb://localhost:27222', { serverSelectionTimeoutMS: 1000 diff --git a/test/unit/client-side-encryption/errors.test.ts b/test/unit/client-side-encryption/errors.test.ts new file mode 100644 index 0000000000..6eaa0dbd6b --- /dev/null +++ b/test/unit/client-side-encryption/errors.test.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-restricted-imports */ +import { expect } from 'chai'; + +import { + MongoCryptAzureKMSRequestError, + MongoCryptCreateDataKeyError, + MongoCryptCreateEncryptedCollectionError, + MongoCryptError, + MongoCryptInvalidArgumentError +} from '../../../src/client-side-encryption/errors'; +import { MongoError } from '../../mongodb'; + +describe('MongoCryptError', function () { + const errors = [ + new MongoCryptAzureKMSRequestError(''), + new MongoCryptCreateDataKeyError( + { + encryptedFields: {} + }, + { + cause: new Error() + } + ), + new MongoCryptCreateEncryptedCollectionError( + { + encryptedFields: {} + }, + { cause: new Error() } + ), + new MongoCryptError(''), + new MongoCryptInvalidArgumentError('') + ]; + + for (const err of errors) { + describe(err.name, function () { + it('is subclass of MongoError', function () { + expect(err).to.be.instanceOf(MongoError); + }); + }); + } +}); diff --git a/test/unit/error.test.ts b/test/unit/error.test.ts index f995a8ff62..ebaa0e9281 100644 --- a/test/unit/error.test.ts +++ b/test/unit/error.test.ts @@ -90,16 +90,17 @@ describe('MongoErrors', () => { expect(err).to.be.an.instanceof(Error); expect(err.name).to.equal('MongoError'); expect(err.message).to.equal(errorMessage); + expect(err).to.not.have.property('cause'); }); - it('should accept an Error object', () => { + it('should accept options and set cause property', () => { const errorMessage = 'A test error'; const inputError = new Error(errorMessage); - const err = new MongoError(inputError); + const err = new MongoError('test', { cause: inputError }); expect(err).to.be.an.instanceof(Error); expect(err.name).to.equal('MongoError'); - expect(err.message).to.equal(errorMessage); - expect(err).to.have.property('cause', inputError); + expect(err.message).to.equal('test'); + expect(err).to.have.property('cause').that.is.instanceOf(Error); }); }); @@ -169,7 +170,7 @@ describe('MongoErrors', () => { context('when options.cause is not set', () => { it('attaches the cause property to the instance', () => { const error = new MongoMissingDependencyError('missing!', { cause: undefined }); - expect(error).to.not.have.property('cause'); + expect(error).to.have.property('cause').that.is.undefined; }); }); }); @@ -661,4 +662,48 @@ describe('MongoErrors', () => { }); }); }); + + describe('MongoError#buildErrorMessage', function () { + context( + 'when passed an AggregateError with an empty message and non-empty errors array', + function () { + it('returns error messages separated by commas', function () { + const aggErr = new AggregateError([new Error('message 1'), new Error('message 2')], ''); + expect(MongoError.buildErrorMessage(aggErr)).to.deep.equal('message 1, message 2'); + }); + } + ); + context('when passed an AggregateError with a non-empty message', function () { + it('returns message field', function () { + const aggErr = new AggregateError( + [new Error('message 1'), new Error('message 2')], + 'aggErr' + ); + expect(MongoError.buildErrorMessage(aggErr)).to.deep.equal(aggErr.message); + }); + }); + context( + 'when passed an AggregateError with an empty errors array and empty message', + function () { + it('returns string instructing user to check `cause` property', function () { + const aggErr = new AggregateError([], ''); + expect(MongoError.buildErrorMessage(aggErr)).to.match( + /Please check the `cause` property for more information./ + ); + }); + } + ); + context('when passed an Error that is not an AggregateError', function () { + it("returns the Error's message property", function () { + const err = new Error('error message'); + expect(MongoError.buildErrorMessage(err)).to.deep.equal('error message'); + }); + }); + + context('when passed a string', function () { + it('returns the string', function () { + expect(MongoError.buildErrorMessage('message')).to.deep.equal('message'); + }); + }); + }); });