Skip to content

Commit 93e6f70

Browse files
authored
chore(shell-api): rephrase errors with offensive terminology MONGOSH-735 (#910)
1 parent 944aaf1 commit 93e6f70

File tree

4 files changed

+160
-6
lines changed

4 files changed

+160
-6
lines changed

packages/cli-repl/test/e2e-direct.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe('e2e direct connection', () => {
9494
await shell.waitForPrompt();
9595
await shell.executeLine('use admin');
9696
await shell.executeLine('db.runCommand({ listCollections: 1 })');
97-
shell.assertContainsError('MongoError: not master');
97+
shell.assertContainsError('MongoError: not primary');
9898
});
9999

100100
it('lists collections when readPreference is in the connection string', async() => {

packages/shell-api/src/decorators.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import Help from './help';
1313
import { addHiddenDataProperty } from './helpers';
1414
import { checkInterrupted } from './interruptor';
15+
import { rephraseMongoError } from './mongo-errors';
1516

1617
const addSourceToResultsSymbol = Symbol.for('@@mongosh.addSourceToResults');
1718
const resultSource = Symbol.for('@@mongosh.resultSource');
@@ -161,17 +162,27 @@ function wrapWithApiChecks<T extends(...args: any[]) => any>(fn: T, className: s
161162
const internalState = getShellInternalState(this);
162163
checkForDeprecation(internalState, className, fn);
163164
const interrupted = checkInterrupted(internalState);
164-
const result = await Promise.race([
165-
interrupted ? interrupted.asPromise() : new Promise(() => {}),
166-
fn.call(this, ...args)
167-
]);
165+
let result: any;
166+
try {
167+
result = await Promise.race([
168+
interrupted ? interrupted.asPromise() : new Promise(() => {}),
169+
fn.call(this, ...args)
170+
]);
171+
} catch (e) {
172+
throw rephraseMongoError(e);
173+
}
168174
checkInterrupted(internalState);
169175
return result;
170176
}) : function(this: any, ...args: any[]): any {
171177
const internalState = getShellInternalState(this);
172178
checkForDeprecation(internalState, className, fn);
173179
checkInterrupted(internalState);
174-
const result = fn.call(this, ...args);
180+
let result: any;
181+
try {
182+
result = fn.call(this, ...args);
183+
} catch (e) {
184+
throw rephraseMongoError(e);
185+
}
175186
checkInterrupted(internalState);
176187
return result;
177188
};
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { MongoError } from 'mongodb';
2+
import { expect } from 'chai';
3+
import { rephraseMongoError } from './mongo-errors';
4+
import Mongo from './mongo';
5+
import { StubbedInstance, stubInterface } from 'ts-sinon';
6+
import { bson, ServiceProvider } from '@mongosh/service-provider-core';
7+
import Database from './database';
8+
import { EventEmitter } from 'events';
9+
import ShellInternalState from './shell-internal-state';
10+
import Collection from './collection';
11+
12+
class MongoshInternalError extends Error {
13+
constructor(message: string) {
14+
super(message);
15+
this.name = 'MongoshInternalError';
16+
}
17+
}
18+
19+
describe('mongo-errors', () => {
20+
describe('rephraseMongoError', () => {
21+
context('for primitive "errors"', () => {
22+
[
23+
true,
24+
42,
25+
'a message',
26+
{ some: 'object' }
27+
].forEach(e => {
28+
it(`skips ${JSON.stringify(e)}`, () => {
29+
expect(rephraseMongoError(e)).to.equal(e);
30+
});
31+
});
32+
});
33+
34+
context('for non-MongoError errors', () => {
35+
[
36+
new Error('an error'),
37+
Object.assign(new MongoshInternalError('Dummy error'), { code: 13435 })
38+
].forEach(e => {
39+
it(`ignores ${e.constructor.name} ${JSON.stringify(e)}`, () => {
40+
const origMessage = e.message;
41+
const r = rephraseMongoError(e);
42+
expect(r).to.equal(r);
43+
expect(r.message).to.equal(origMessage);
44+
});
45+
});
46+
});
47+
48+
context('for MongoError errors', () => {
49+
it('ignores an irrelevant error', () => {
50+
const e = new MongoError('ignored');
51+
const r = rephraseMongoError(e);
52+
expect(r).to.equal(e);
53+
expect(r.message).to.equal('ignored');
54+
});
55+
56+
it('rephrases a NotPrimaryNoSecondaryOk error', () => {
57+
const e = new MongoError('not master and slaveOk=false');
58+
e.code = 13435;
59+
const r = rephraseMongoError(e);
60+
expect(r).to.equal(e);
61+
expect(r.code).to.equal(13435);
62+
expect(r.message).to.contain('setReadPref');
63+
});
64+
});
65+
});
66+
67+
describe('intercepts shell API calls', () => {
68+
let mongo: Mongo;
69+
let serviceProvider: StubbedInstance<ServiceProvider>;
70+
let database: Database;
71+
let bus: StubbedInstance<EventEmitter>;
72+
let internalState: ShellInternalState;
73+
let collection: Collection;
74+
75+
beforeEach(() => {
76+
bus = stubInterface<EventEmitter>();
77+
serviceProvider = stubInterface<ServiceProvider>();
78+
serviceProvider.runCommand.resolves({ ok: 1 });
79+
serviceProvider.runCommandWithCheck.resolves({ ok: 1 });
80+
serviceProvider.initialDb = 'test';
81+
serviceProvider.bsonLibrary = bson;
82+
internalState = new ShellInternalState(serviceProvider, bus);
83+
mongo = new Mongo(internalState, undefined, undefined, undefined, serviceProvider);
84+
database = new Database(mongo, 'db1');
85+
collection = new Collection(mongo, database, 'coll1');
86+
});
87+
88+
it('on collection.find error', async() => {
89+
const error = new MongoError('not master and slaveOk=false');
90+
error.code = 13435;
91+
serviceProvider.insertOne.rejects(error);
92+
93+
try {
94+
await collection.insertOne({ fails: true });
95+
expect.fail('expected error');
96+
} catch (e) {
97+
expect(e).to.equal(error);
98+
expect(e.message).to.contain('not primary and secondaryOk=false');
99+
expect(e.message).to.contain('db.getMongo().setReadPref()');
100+
expect(e.message).to.contain('readPref');
101+
}
102+
});
103+
});
104+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
interface MongoErrorRephrase {
3+
matchMessage?: RegExp | string;
4+
code?: number;
5+
replacement: ((message: string) => string) | string;
6+
}
7+
const ERROR_REPHRASES: MongoErrorRephrase[] = [
8+
{
9+
// NotPrimaryNoSecondaryOk (also used for old terminology)
10+
code: 13435,
11+
replacement: 'not primary and secondaryOk=false - consider using db.getMongo().setReadPref() or readPref in the connection string'
12+
}
13+
];
14+
15+
export function rephraseMongoError(error: any): any {
16+
if (!isMongoError(error)) {
17+
return error;
18+
}
19+
20+
const e = error as Error;
21+
const message = e.message;
22+
23+
const rephrase = ERROR_REPHRASES.find(m => {
24+
if (m.matchMessage) {
25+
return typeof m.matchMessage === 'string' ? message.includes(m.matchMessage) : m.matchMessage.test(message);
26+
}
27+
return m.code !== undefined && (e as any).code === m.code;
28+
});
29+
30+
if (rephrase) {
31+
e.message = typeof rephrase.replacement === 'function' ? rephrase.replacement(message) : rephrase.replacement;
32+
}
33+
34+
return e;
35+
}
36+
37+
function isMongoError(error: any): boolean {
38+
return /^Mongo([A-Z].*)?Error$/.test(Object.getPrototypeOf(error)?.constructor?.name ?? '');
39+
}

0 commit comments

Comments
 (0)