Skip to content

Commit 5759f84

Browse files
committed
feat(NODE-3920)!: validate options are not repeated in connection string
1 parent ada1f75 commit 5759f84

File tree

2 files changed

+40
-2
lines changed

2 files changed

+40
-2
lines changed

src/connection_string.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ function getUIntFromOptions(name: string, value: unknown): number {
205205
}
206206

207207
function* entriesFromString(value: string): Generator<[string, string]> {
208+
if (value === '') {
209+
return;
210+
}
208211
const keyValuePairs = value.split(',');
209212
for (const keyValue of keyValuePairs) {
210213
const [key, value] = keyValue.split(/:(.*)/);
@@ -291,9 +294,17 @@ export function parseOptions(
291294
}
292295

293296
for (const key of url.searchParams.keys()) {
294-
const values = [...url.searchParams.getAll(key)];
297+
const values = url.searchParams.getAll(key);
298+
299+
const isReadPreferenceTags = /readPreferenceTags/i.test(key);
300+
301+
if (!isReadPreferenceTags && values.length > 1) {
302+
throw new MongoInvalidArgumentError(
303+
`URI option "${key}" cannot appear more than once in the connection string`
304+
);
305+
}
295306

296-
if (values.includes('')) {
307+
if (!isReadPreferenceTags && values.includes('')) {
297308
throw new MongoAPIError('URI cannot contain options with no value');
298309
}
299310

test/unit/connection_string.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,19 @@ describe('Connection String', function () {
147147
{ baz: 'bar' }
148148
]);
149149
});
150+
151+
it('should parse multiple and empty readPreferenceTags', () => {
152+
const options = parseOptions(
153+
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=baz:bar&readPreferenceTags='
154+
);
155+
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }, {}]);
156+
});
157+
158+
it('will set "__proto__" as own property on readPreferenceTag', () => {
159+
const options = parseOptions('mongodb://hostname?readPreferenceTags=__proto__:foo');
160+
expect(options.readPreference.tags?.[0]).to.have.own.property('__proto__', 'foo');
161+
expect(Object.getPrototypeOf(options.readPreference.tags?.[0])).to.be.null;
162+
});
150163
});
151164

152165
context('when the option is passed in the options object', () => {
@@ -156,12 +169,14 @@ describe('Connection String', function () {
156169
});
157170
expect(options.readPreference.tags).to.deep.equal([]);
158171
});
172+
159173
it('should parse a single readPreferenceTags object', () => {
160174
const options = parseOptions('mongodb://hostname?', {
161175
readPreferenceTags: [{ bar: 'foo' }]
162176
});
163177
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }]);
164178
});
179+
165180
it('should parse multiple readPreferenceTags', () => {
166181
const options = parseOptions('mongodb://hostname?', {
167182
readPreferenceTags: [{ bar: 'foo' }, { baz: 'bar' }]
@@ -493,6 +508,18 @@ describe('Connection String', function () {
493508
);
494509
});
495510

511+
it('throws an error for repeated options that can only appear once', function () {
512+
// At the time of writing, readPreferenceTags is the only options that can be repeated
513+
let thrownError;
514+
try {
515+
parseOptions('mongodb://localhost/?compressors=zstd&compressors=zstd');
516+
} catch (error) {
517+
thrownError = error;
518+
}
519+
expect(thrownError).to.be.instanceOf(MongoInvalidArgumentError);
520+
expect(thrownError.message).to.match(/cannot appear more than once/);
521+
});
522+
496523
it('should validate authMechanism', function () {
497524
expect(() => parseOptions('mongodb://localhost/?authMechanism=DOGS')).to.throw(
498525
MongoParseError,

0 commit comments

Comments
 (0)