Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli-repl/test/e2e-direct.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('e2e direct connection', () => {
await shell.waitForPrompt();
await shell.executeLine('use admin');
await shell.executeLine('db.runCommand({ listCollections: 1 })');
shell.assertContainsError('MongoError: not master');
shell.assertContainsError('MongoError: not primary');
});

it('lists collections when readPreference is in the connection string', async() => {
Expand Down
21 changes: 16 additions & 5 deletions packages/shell-api/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import Help from './help';
import { addHiddenDataProperty } from './helpers';
import { checkInterrupted } from './interruptor';
import { rephraseMongoError } from './mongo-errors';

const addSourceToResultsSymbol = Symbol.for('@@mongosh.addSourceToResults');
const resultSource = Symbol.for('@@mongosh.resultSource');
Expand Down Expand Up @@ -161,17 +162,27 @@ function wrapWithApiChecks<T extends(...args: any[]) => any>(fn: T, className: s
const internalState = getShellInternalState(this);
checkForDeprecation(internalState, className, fn);
const interrupted = checkInterrupted(internalState);
const result = await Promise.race([
interrupted ? interrupted.asPromise() : new Promise(() => {}),
fn.call(this, ...args)
]);
let result: any;
try {
result = await Promise.race([
interrupted ? interrupted.asPromise() : new Promise(() => {}),
fn.call(this, ...args)
]);
} catch (e) {
throw rephraseMongoError(e);
}
checkInterrupted(internalState);
return result;
}) : function(this: any, ...args: any[]): any {
const internalState = getShellInternalState(this);
checkForDeprecation(internalState, className, fn);
checkInterrupted(internalState);
const result = fn.call(this, ...args);
let result: any;
try {
result = fn.call(this, ...args);
} catch (e) {
throw rephraseMongoError(e);
}
checkInterrupted(internalState);
return result;
};
Expand Down
104 changes: 104 additions & 0 deletions packages/shell-api/src/mongo-errors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { MongoError } from 'mongodb';
import { expect } from 'chai';
import { rephraseMongoError } from './mongo-errors';
import Mongo from './mongo';
import { StubbedInstance, stubInterface } from 'ts-sinon';
import { bson, ServiceProvider } from '@mongosh/service-provider-core';
import Database from './database';
import { EventEmitter } from 'events';
import ShellInternalState from './shell-internal-state';
import Collection from './collection';

class MongoshInternalError extends Error {
constructor(message: string) {
super(message);
this.name = 'MongoshInternalError';
}
}

describe('mongo-errors', () => {
describe('rephraseMongoError', () => {
context('for primitive "errors"', () => {
[
true,
42,
'a message',
{ some: 'object' }
].forEach(e => {
it(`skips ${JSON.stringify(e)}`, () => {
expect(rephraseMongoError(e)).to.equal(e);
});
});
});

context('for non-MongoError errors', () => {
[
new Error('an error'),
Object.assign(new MongoshInternalError('Dummy error'), { code: 13435 })
].forEach(e => {
it(`ignores ${e.constructor.name} ${JSON.stringify(e)}`, () => {
const origMessage = e.message;
const r = rephraseMongoError(e);
expect(r).to.equal(r);
expect(r.message).to.equal(origMessage);
});
});
});

context('for MongoError errors', () => {
it('ignores an irrelevant error', () => {
const e = new MongoError('ignored');
const r = rephraseMongoError(e);
expect(r).to.equal(e);
expect(r.message).to.equal('ignored');
});

it('rephrases a NotPrimaryNoSecondaryOk error', () => {
const e = new MongoError('not master and slaveOk=false');
e.code = 13435;
const r = rephraseMongoError(e);
expect(r).to.equal(e);
expect(r.code).to.equal(13435);
expect(r.message).to.contain('setReadPref');
});
});
});

describe('intercepts shell API calls', () => {
let mongo: Mongo;
let serviceProvider: StubbedInstance<ServiceProvider>;
let database: Database;
let bus: StubbedInstance<EventEmitter>;
let internalState: ShellInternalState;
let collection: Collection;

beforeEach(() => {
bus = stubInterface<EventEmitter>();
serviceProvider = stubInterface<ServiceProvider>();
serviceProvider.runCommand.resolves({ ok: 1 });
serviceProvider.runCommandWithCheck.resolves({ ok: 1 });
serviceProvider.initialDb = 'test';
serviceProvider.bsonLibrary = bson;
internalState = new ShellInternalState(serviceProvider, bus);
mongo = new Mongo(internalState, undefined, undefined, undefined, serviceProvider);
database = new Database(mongo, 'db1');
collection = new Collection(mongo, database, 'coll1');
});

it('on collection.find error', async() => {
const error = new MongoError('not master and slaveOk=false');
error.code = 13435;
serviceProvider.insertOne.rejects(error);

try {
await collection.insertOne({ fails: true });
expect.fail('expected error');
} catch (e) {
expect(e).to.equal(error);
expect(e.message).to.contain('not primary and secondaryOk=false');
expect(e.message).to.contain('db.getMongo().setReadPref()');
expect(e.message).to.contain('readPref');
}
});
});
});
39 changes: 39 additions & 0 deletions packages/shell-api/src/mongo-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

interface MongoErrorRephrase {
matchMessage?: RegExp | string;
code?: number;
replacement: ((message: string) => string) | string;
}
const ERROR_REPHRASES: MongoErrorRephrase[] = [
{
// NotPrimaryNoSecondaryOk (also used for old terminology)
code: 13435,
replacement: 'not primary and secondaryOk=false - consider using db.getMongo().setReadPref() or readPref in the connection string'
}
];

export function rephraseMongoError(error: any): any {
if (!isMongoError(error)) {
return error;
}

const e = error as Error;
const message = e.message;

const rephrase = ERROR_REPHRASES.find(m => {
if (m.matchMessage) {
return typeof m.matchMessage === 'string' ? message.includes(m.matchMessage) : m.matchMessage.test(message);
}
return m.code !== undefined && (e as any).code === m.code;
});

if (rephrase) {
e.message = typeof rephrase.replacement === 'function' ? rephrase.replacement(message) : rephrase.replacement;
}

return e;
}

function isMongoError(error: any): boolean {
return /^Mongo([A-Z].*)?Error$/.test(Object.getPrototypeOf(error)?.constructor?.name ?? '');
}