Skip to content

Commit f20b109

Browse files
committed
feat(devtools-connect): fail fast on specific error and codes from compass-web COMPASS-9793
1 parent ff08cc5 commit f20b109

File tree

5 files changed

+133
-10
lines changed

5 files changed

+133
-10
lines changed

packages/devtools-connect/src/connect.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,23 @@ describe('devtools connect', function () {
375375
expect(result.client).to.equal(mClient);
376376
});
377377

378+
it.only('allows useSystemCA to be configured', async function () {
379+
const uri = 'localhost:27017';
380+
const mClient = stubConstructor(FakeMongoClient);
381+
const mClientType = sinon.stub().returns(mClient);
382+
mClient.connect.onFirstCall().resolves(mClient);
383+
const result = await connectMongoClient(
384+
uri,
385+
{ ...defaultOpts, useSystemCA: false },
386+
bus,
387+
mClientType as any,
388+
);
389+
expect(mClientType.getCalls()).to.have.lengthOf(1);
390+
expect(mClientType.getCalls()[0].args[1].ca).to.equal(undefined);
391+
expect(mClient.connect.getCalls()).to.have.lengthOf(1);
392+
expect(result.client).to.equal(mClient);
393+
});
394+
378395
describe('retryable TLS errors', function () {
379396
it('retries TLS errors without system CA integration enabled -- MongoClient error', async function () {
380397
const uri = 'localhost:27017';

packages/devtools-connect/src/connect.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ export interface DevtoolsConnectOptions extends MongoClientOptions {
406406
* provided in `proxy` for OIDC traffic.
407407
*/
408408
applyProxyToOIDC?: boolean | DevtoolsProxyOptions | AgentWithInitialize;
409+
/**
410+
* Whether to use the system certificate store. Defaults to `true`.
411+
*/
412+
useSystemCA?: boolean;
409413
}
410414

411415
export type ConnectMongoClientResult = {
@@ -426,7 +430,15 @@ export async function connectMongoClient(
426430
): Promise<ConnectMongoClientResult> {
427431
detectAndLogMissingOptionalDependencies(logger);
428432

429-
const options = { uri, clientOptions, logger, MongoClientClass };
433+
const options = {
434+
uri,
435+
clientOptions,
436+
logger,
437+
MongoClientClass,
438+
useSystemCA: clientOptions.useSystemCA ?? true,
439+
};
440+
delete clientOptions.useSystemCA;
441+
430442
// Connect once with the system certificate store added, and if that fails with
431443
// a TLS error, try again. In theory adding certificates into the certificate store
432444
// should not cause failures, but in practice we have observed some, hence this
@@ -435,7 +447,7 @@ export async function connectMongoClient(
435447
// failure situations) we do not spend an unreasonable amount of time in the first
436448
// connection attempt.
437449
try {
438-
return await connectMongoClientImpl({ ...options, useSystemCA: true });
450+
return await connectMongoClientImpl({ ...options });
439451
} catch (error: unknown) {
440452
if (isPotentialTLSCertificateError(error)) {
441453
logger.emit('devtools-connect:retry-after-tls-error', {

packages/devtools-connect/src/fast-failure-connect.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,71 @@ describe('isFastFailureConnectionError', function () {
4646
isFastFailureConnectionError(new Error('could not connect')),
4747
).to.equal(false);
4848
});
49+
50+
describe('isCompassSocketServiceError', function () {
51+
class CompassSocketServiceError extends Error {
52+
constructor(
53+
msg: string,
54+
public code: number,
55+
) {
56+
super(msg);
57+
this.name = 'CompassSocketServiceError';
58+
}
59+
}
60+
61+
it('returns true for UNAUTHORIZED (3000)', function () {
62+
const error = new CompassSocketServiceError('Unauthorized', 3000);
63+
expect(isFastFailureConnectionError(error)).to.equal(true);
64+
});
65+
66+
it('returns true for FORBIDDEN (3003)', function () {
67+
const error = new CompassSocketServiceError('Forbidden', 3003);
68+
expect(isFastFailureConnectionError(error)).to.equal(true);
69+
});
70+
71+
it('returns true for NOT_FOUND (4004)', function () {
72+
const error = new CompassSocketServiceError('Not found', 4004);
73+
expect(isFastFailureConnectionError(error)).to.equal(true);
74+
});
75+
76+
it('returns true for VIOLATED_POLICY (1008)', function () {
77+
const error = new CompassSocketServiceError('Violated policy', 1008);
78+
expect(isFastFailureConnectionError(error)).to.equal(true);
79+
});
80+
81+
it('returns true for DO_NOT_TRY_AGAIN (4101)', function () {
82+
const error = new CompassSocketServiceError('Do not try again', 4101);
83+
expect(isFastFailureConnectionError(error)).to.equal(true);
84+
});
85+
86+
it('returns false for CompassSocketServiceError with non-fail-fast code', function () {
87+
const error = new CompassSocketServiceError('Some other error', 9999);
88+
expect(isFastFailureConnectionError(error)).to.equal(false);
89+
});
90+
91+
it('returns true when CompassSocketServiceError is the cause of MongoNetworkError', function () {
92+
const cause = new CompassSocketServiceError('Unauthorized', 3000);
93+
const error = new MongoNetworkError('Connection failed');
94+
(error as any).cause = cause;
95+
expect(isFastFailureConnectionError(error)).to.equal(true);
96+
});
97+
98+
it('returns true when CompassSocketServiceError is nested deeply', function () {
99+
const cause = new CompassSocketServiceError('Forbidden', 3003);
100+
const wrappedError = new Error('Wrapped error');
101+
(wrappedError as any).cause = cause;
102+
const error = new MongoNetworkError('Connection failed');
103+
(error as any).cause = wrappedError;
104+
expect(isFastFailureConnectionError(error)).to.equal(true);
105+
});
106+
107+
it('returns true when CompassSocketServiceError is in an AggregateError', function () {
108+
const cause = new CompassSocketServiceError('Not found', 4004);
109+
const aggregateError = new AggregateError(
110+
[new Error('Other error'), cause],
111+
'Multiple errors',
112+
);
113+
expect(isFastFailureConnectionError(aggregateError)).to.equal(true);
114+
});
115+
});
49116
});

packages/devtools-connect/src/fast-failure-connect.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,48 @@
11
// It probably makes sense to put this into its own package/repository once
22
// other tools start using it.
33

4+
const NODE_SOCKET_NON_RETRY_CODES = [
5+
'ECONNREFUSED',
6+
'ENOTFOUND',
7+
'ENETUNREACH',
8+
'EINVAL',
9+
];
10+
const COMPASS_SOCKET_SERVICE_NON_RETRY_CODES = [
11+
3000, // UNAUTHORIZED
12+
3003, // FORBIDDEN
13+
4004, // NOT_FOUND
14+
1008, // VIOLATED_POLICY
15+
4101, // DO_NOT_TRY_AGAIN
16+
];
17+
18+
const isCompassSocketServiceError = handleNestedErrors(
19+
(error: Error & { code?: string | number }): boolean => {
20+
if (error.name === 'CompassSocketServiceError') {
21+
return (
22+
typeof error.code === 'number' &&
23+
COMPASS_SOCKET_SERVICE_NON_RETRY_CODES.includes(error.code)
24+
);
25+
}
26+
return false;
27+
},
28+
);
29+
430
function isFastFailureConnectionSingleError(
5-
error: Error & { code?: string },
31+
error: Error & { code?: string | number },
632
): boolean {
733
switch (error.name) {
834
case 'MongoNetworkError':
9-
return /\b(ECONNREFUSED|ENOTFOUND|ENETUNREACH|EINVAL)\b/.test(
10-
error.message,
11-
);
35+
return new RegExp(
36+
String.raw`\b(${NODE_SOCKET_NON_RETRY_CODES.join('|')})\b`,
37+
).test(error.message);
1238
case 'MongoError':
1339
return /The apiVersion parameter is required/.test(error.message);
1440
default:
1541
return (
16-
['ECONNREFUSED', 'ENOTFOUND', 'ENETUNREACH', 'EINVAL'].includes(
17-
error.code ?? '',
18-
) || isPotentialTLSCertificateError(error)
42+
(typeof error.code === 'string' &&
43+
NODE_SOCKET_NON_RETRY_CODES.includes(error.code)) ||
44+
isPotentialTLSCertificateError(error) ||
45+
isCompassSocketServiceError(error)
1946
);
2047
}
2148
}

packages/devtools-connect/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"extends": "@mongodb-js/tsconfig-devtools/tsconfig.common.json",
33
"compilerOptions": {
44
"target": "es2020",
5-
"lib": ["es2020", "DOM"],
5+
"lib": ["es2021", "DOM"],
66
"module": "commonjs",
77
"moduleResolution": "node"
88
}

0 commit comments

Comments
 (0)