From 98b7bdf2d6c08ba9470bbda0fad3251ea0a08949 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 23 May 2023 14:02:48 -0600 Subject: [PATCH] fix(NODE-5311): construct error messages for AggregateErrors in Node16+ (#3683) --- global.d.ts | 1 + src/error.ts | 23 ++++++-- test/integration/node-specific/errors.ts | 54 +++++++++++++++++++ .../runner/filters/node_version_filter.js | 25 +++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 test/integration/node-specific/errors.ts create mode 100644 test/tools/runner/filters/node_version_filter.js diff --git a/global.d.ts b/global.d.ts index 83c66eace0..cf1b75dae7 100644 --- a/global.d.ts +++ b/global.d.ts @@ -11,6 +11,7 @@ declare global { clientSideEncryption?: boolean; serverless?: 'forbid' | 'allow' | 'require'; auth?: 'enabled' | 'disabled'; + nodejs?: string; }; sessions?: { diff --git a/src/error.ts b/src/error.ts index 0e9590423f..6e5f6a515a 100644 --- a/src/error.ts +++ b/src/error.ts @@ -107,6 +107,10 @@ export interface ErrorDescription extends Document { errInfo?: Document; } +function isAggregateError(e: Error): e is Error & { errors: Error[] } { + return 'errors' in e && Array.isArray(e.errors); +} + /** * @public * @category Error @@ -130,15 +134,28 @@ export class MongoError extends Error { 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) { - super(message.message); this.cause = message; - } else { - super(message); } + this[kErrorLabels] = new Set(); } + /** @internal */ + private static buildErrorMessage(e: Error | string): string { + if (typeof e === 'string') { + return e; + } + if (isAggregateError(e) && e.message.length === 0) { + return e.errors.length === 0 + ? 'AggregateError has an empty errors array. Please check the `cause` property for more information.' + : e.errors.map(({ message }) => message).join(', '); + } + + return e.message; + } + override get name(): string { return 'MongoError'; } diff --git a/test/integration/node-specific/errors.ts b/test/integration/node-specific/errors.ts new file mode 100644 index 0000000000..c487f29cfc --- /dev/null +++ b/test/integration/node-specific/errors.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; + +import { MongoClient, MongoError, MongoServerSelectionError } from '../../../src'; + +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`, + { requires: { nodejs: '>=16' } }, + () => { + 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`, { requires: { nodejs: '>=16' } }, () => { + 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', { requires: { nodejs: '>=16' } }, () => { + 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 + }).catch(e => e); + expect(error).to.be.instanceOf(MongoServerSelectionError); + expect(error.message).not.to.be.empty; + }); +}); diff --git a/test/tools/runner/filters/node_version_filter.js b/test/tools/runner/filters/node_version_filter.js new file mode 100644 index 0000000000..61c11adc64 --- /dev/null +++ b/test/tools/runner/filters/node_version_filter.js @@ -0,0 +1,25 @@ +'use strict'; + +const { satisfies } = require('semver'); + +/** + * Filter for specific nodejs versions + * + * example: + * metadata: { + * requires: { + * nodejs: '>=14' + * } + * } + */ +class NodeVersionFilter { + filter(test) { + if (!test.metadata) return true; + if (!test.metadata.requires) return true; + if (!test.metadata.requires.nodejs) return true; + + return satisfies(process.version, test.metadata.requires.nodejs); + } +} + +module.exports = NodeVersionFilter;