Skip to content

Commit 05c230c

Browse files
fix(NODE-7138): prevent duplicate metadata from being appended to handshake metadata (#4651)
1 parent aa7465e commit 05c230c

File tree

9 files changed

+705
-146
lines changed

9 files changed

+705
-146
lines changed

src/cmap/handshake/client_metadata.ts

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,30 @@ import * as os from 'os';
22
import * as process from 'process';
33

44
import { BSON, type Document, Int32 } from '../../bson';
5-
import { MongoInvalidArgumentError, MongoRuntimeError } from '../../error';
6-
import type { MongoOptions } from '../../mongo_client';
5+
import { MongoInvalidArgumentError } from '../../error';
6+
import type { DriverInfo, MongoOptions } from '../../mongo_client';
77
import { fileIsAccessible } from '../../utils';
88

99
// eslint-disable-next-line @typescript-eslint/no-require-imports
1010
const NODE_DRIVER_VERSION = require('../../../package.json').version;
1111

12+
/** @internal */
13+
export function isDriverInfoEqual(info1: DriverInfo, info2: DriverInfo): boolean {
14+
/** for equality comparison, we consider "" as unset */
15+
const nonEmptyCmp = (s1: string | undefined, s2: string | undefined): boolean => {
16+
s1 ||= undefined;
17+
s2 ||= undefined;
18+
19+
return s1 === s2;
20+
};
21+
22+
return (
23+
nonEmptyCmp(info1.name, info2.name) &&
24+
nonEmptyCmp(info1.platform, info2.platform) &&
25+
nonEmptyCmp(info1.version, info2.version)
26+
);
27+
}
28+
1229
/**
1330
* @public
1431
* @deprecated This interface will be made internal in the next major release.
@@ -90,10 +107,7 @@ export class LimitedSizeDocument {
90107
}
91108
}
92109

93-
type MakeClientMetadataOptions = Pick<
94-
MongoOptions,
95-
'appName' | 'driverInfo' | 'additionalDriverInfo'
96-
>;
110+
type MakeClientMetadataOptions = Pick<MongoOptions, 'appName'>;
97111
/**
98112
* From the specs:
99113
* Implementors SHOULD cumulatively update fields in the following order until the document is under the size limit:
@@ -102,34 +116,28 @@ type MakeClientMetadataOptions = Pick<
102116
* 3. Omit the `env` document entirely.
103117
* 4. Truncate `platform`. -- special we do not truncate this field
104118
*/
105-
export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMetadata {
119+
export function makeClientMetadata(
120+
driverInfoList: DriverInfo[],
121+
{ appName = '' }: MakeClientMetadataOptions
122+
): ClientMetadata {
106123
const metadataDocument = new LimitedSizeDocument(512);
107124

108-
const { appName = '' } = options;
109125
// Add app name first, it must be sent
110126
if (appName.length > 0) {
111127
const name =
112128
Buffer.byteLength(appName, 'utf8') <= 128
113-
? options.appName
129+
? appName
114130
: Buffer.from(appName, 'utf8').subarray(0, 128).toString('utf8');
115131
metadataDocument.ifItFitsItSits('application', { name });
116132
}
117133

118-
const { name = '', version = '', platform = '' } = options.driverInfo;
119-
120134
const driverInfo = {
121-
name: name.length > 0 ? `nodejs|${name}` : 'nodejs',
122-
version: version.length > 0 ? `${NODE_DRIVER_VERSION}|${version}` : NODE_DRIVER_VERSION
135+
name: 'nodejs',
136+
version: NODE_DRIVER_VERSION
123137
};
124138

125-
if (options.additionalDriverInfo == null) {
126-
throw new MongoRuntimeError(
127-
'Client options `additionalDriverInfo` must always default to an empty array'
128-
);
129-
}
130-
131139
// This is where we handle additional driver info added after client construction.
132-
for (const { name: n = '', version: v = '' } of options.additionalDriverInfo) {
140+
for (const { name: n = '', version: v = '' } of driverInfoList) {
133141
if (n.length > 0) {
134142
driverInfo.name = `${driverInfo.name}|${n}`;
135143
}
@@ -145,13 +153,10 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
145153
}
146154

147155
let runtimeInfo = getRuntimeInfo();
148-
if (platform.length > 0) {
149-
runtimeInfo = `${runtimeInfo}|${platform}`;
150-
}
151-
152-
for (const { platform: p = '' } of options.additionalDriverInfo) {
153-
if (p.length > 0) {
154-
runtimeInfo = `${runtimeInfo}|${p}`;
156+
// This is where we handle additional driver info added after client construction.
157+
for (const { platform = '' } of driverInfoList) {
158+
if (platform.length > 0) {
159+
runtimeInfo = `${runtimeInfo}|${platform}`;
155160
}
156161
}
157162

@@ -210,7 +215,9 @@ async function getContainerMetadata() {
210215
* Re-add each metadata value.
211216
* Attempt to add new env container metadata, but keep old data if it does not fit.
212217
*/
213-
export async function addContainerMetadata(originalMetadata: ClientMetadata) {
218+
export async function addContainerMetadata(
219+
originalMetadata: ClientMetadata
220+
): Promise<ClientMetadata> {
214221
const containerMetadata = await getContainerMetadata();
215222
if (Object.keys(containerMetadata).length === 0) return originalMetadata;
216223

@@ -233,7 +240,7 @@ export async function addContainerMetadata(originalMetadata: ClientMetadata) {
233240
extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata);
234241
}
235242

236-
return extendedMetadata.toObject();
243+
return extendedMetadata.toObject() as ClientMetadata;
237244
}
238245

239246
/**

src/connection_string.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { URLSearchParams } from 'url';
55
import type { Document } from './bson';
66
import { MongoCredentials } from './cmap/auth/mongo_credentials';
77
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers';
8-
import { addContainerMetadata, makeClientMetadata } from './cmap/handshake/client_metadata';
98
import { Compressor, type CompressorName } from './cmap/wire_protocol/compression';
109
import { Encrypter } from './encrypter';
1110
import {
@@ -535,16 +534,6 @@ export function parseOptions(
535534
}
536535
);
537536

538-
// Set the default for the additional driver info.
539-
mongoOptions.additionalDriverInfo = [];
540-
541-
mongoOptions.metadata = makeClientMetadata(mongoOptions);
542-
543-
mongoOptions.extendedMetadata = addContainerMetadata(mongoOptions.metadata).then(
544-
undefined,
545-
squashError
546-
); // rejections will be handled later
547-
548537
return mongoOptions;
549538
}
550539

src/mongo_client.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { Connection } from './cmap/connection';
1818
import {
1919
addContainerMetadata,
2020
type ClientMetadata,
21+
isDriverInfoEqual,
2122
makeClientMetadata
2223
} from './cmap/handshake/client_metadata';
2324
import type { CompressorName } from './cmap/wire_protocol/compression';
@@ -433,12 +434,16 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
433434
| 'extendedMetadata'
434435
>;
435436

437+
private driverInfoList: DriverInfo[] = [];
438+
436439
constructor(url: string, options?: MongoClientOptions) {
437440
super();
438441
this.on('error', noop);
439442

440443
this.options = parseOptions(url, this, options);
441444

445+
this.appendMetadata(this.options.driverInfo);
446+
442447
const shouldSetLogger = Object.values(this.options.mongoLoggerOptions.componentSeverities).some(
443448
value => value !== SeverityLevel.OFF
444449
);
@@ -495,8 +500,13 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
495500
* @param driverInfo - Information about the application or library.
496501
*/
497502
appendMetadata(driverInfo: DriverInfo) {
498-
this.options.additionalDriverInfo.push(driverInfo);
499-
this.options.metadata = makeClientMetadata(this.options);
503+
const isDuplicateDriverInfo = this.driverInfoList.some(info =>
504+
isDriverInfoEqual(info, driverInfo)
505+
);
506+
if (isDuplicateDriverInfo) return;
507+
508+
this.driverInfoList.push(driverInfo);
509+
this.options.metadata = makeClientMetadata(this.driverInfoList, this.options);
500510
this.options.extendedMetadata = addContainerMetadata(this.options.metadata)
501511
.then(undefined, squashError)
502512
.then(result => result ?? {}); // ensure Promise<Document>

test/integration/connection-monitoring-and-pooling/connection.test.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,8 @@ describe('Connection', function () {
4949
...commonConnectOptions,
5050
connectionType: Connection,
5151
...this.configuration.options,
52-
metadata: makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] }),
53-
extendedMetadata: addContainerMetadata(
54-
makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] })
55-
)
52+
metadata: makeClientMetadata([], {}),
53+
extendedMetadata: addContainerMetadata(makeClientMetadata([], {}))
5654
};
5755

5856
let conn;
@@ -74,10 +72,8 @@ describe('Connection', function () {
7472
connectionType: Connection,
7573
...this.configuration.options,
7674
monitorCommands: true,
77-
metadata: makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] }),
78-
extendedMetadata: addContainerMetadata(
79-
makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] })
80-
)
75+
metadata: makeClientMetadata([], {}),
76+
extendedMetadata: addContainerMetadata(makeClientMetadata([], {}))
8177
};
8278

8379
let conn;
@@ -108,10 +104,8 @@ describe('Connection', function () {
108104
connectionType: Connection,
109105
...this.configuration.options,
110106
monitorCommands: true,
111-
metadata: makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] }),
112-
extendedMetadata: addContainerMetadata(
113-
makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] })
114-
)
107+
metadata: makeClientMetadata([], {}),
108+
extendedMetadata: addContainerMetadata(makeClientMetadata([], {}))
115109
};
116110

117111
let conn;

0 commit comments

Comments
 (0)