Skip to content

Commit ef0b96f

Browse files
IamLizuljharb
authored andcommittedNov 12, 2024··
[New] parse: add throwOnParameterLimitExceeded option
1 parent f1ee037 commit ef0b96f

File tree

3 files changed

+162
-19
lines changed

3 files changed

+162
-19
lines changed
 

‎README.md

+24
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
135135
assert.deepEqual(limited, { a: 'b' });
136136
```
137137

138+
If you want an error to be thrown whenever the a limit is exceeded (eg, `parameterLimit`, `arrayLimit`), set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit.
139+
```javascript
140+
try {
141+
qs.parse('a=1&b=2&c=3&d=4', { parameterLimit: 3, throwOnLimitExceeded: true });
142+
} catch (err) {
143+
assert(err instanceof Error);
144+
assert.strictEqual(err.message, 'Parameter limit exceeded. Only 3 parameters allowed.');
145+
}
146+
```
147+
148+
When `throwOnLimitExceeded` is set to `false` (default), **qs** will parse up to the specified `parameterLimit` and ignore the rest without throwing an error.
149+
138150
To bypass the leading question mark, use `ignoreQueryPrefix`:
139151

140152
```javascript
@@ -286,6 +298,18 @@ var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 });
286298
assert.deepEqual(withArrayLimit, { a: { '1': 'b' } });
287299
```
288300

301+
If you want to throw an error whenever the array limit is exceeded, set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit.
302+
```javascript
303+
try {
304+
qs.parse('a[1]=b', { arrayLimit: 0, throwOnLimitExceeded: true });
305+
} catch (err) {
306+
assert(err instanceof Error);
307+
assert.strictEqual(err.message, 'Array limit exceeded. Only 0 elements allowed in an array.');
308+
}
309+
```
310+
311+
When `throwOnLimitExceeded` is set to `false` (default), **qs** will parse up to the specified `arrayLimit` and if the limit is exceeded, the array will instead be converted to an object with the index as the key
312+
289313
To disable array parsing entirely, set `parseArrays` to `false`.
290314

291315
```javascript

‎lib/parse.js

+35-5
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@ var interpretNumericEntities = function (str) {
3434
});
3535
};
3636

37-
var parseArrayValue = function (val, options) {
37+
var parseArrayValue = function (val, options, currentArrayLength) {
3838
if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
3939
return val.split(',');
4040
}
4141

42+
if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
43+
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
44+
}
45+
4246
return val;
4347
};
4448

@@ -57,8 +61,17 @@ var parseValues = function parseQueryStringValues(str, options) {
5761

5862
var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
5963
cleanStr = cleanStr.replace(/%5B/gi, '[').replace(/%5D/gi, ']');
64+
6065
var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit;
61-
var parts = cleanStr.split(options.delimiter, limit);
66+
var parts = cleanStr.split(
67+
options.delimiter,
68+
options.throwOnLimitExceeded ? limit + 1 : limit
69+
);
70+
71+
if (options.throwOnLimitExceeded && parts.length > limit) {
72+
throw new RangeError('Parameter limit exceeded. Only ' + limit + ' parameter' + (limit === 1 ? '' : 's') + ' allowed.');
73+
}
74+
6275
var skipIndex = -1; // Keep track of where the utf8 sentinel was found
6376
var i;
6477

@@ -93,8 +106,13 @@ var parseValues = function parseQueryStringValues(str, options) {
93106
val = options.strictNullHandling ? null : '';
94107
} else {
95108
key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');
109+
96110
val = utils.maybeMap(
97-
parseArrayValue(part.slice(pos + 1), options),
111+
parseArrayValue(
112+
part.slice(pos + 1),
113+
options,
114+
isArray(obj[key]) ? obj[key].length : 0
115+
),
98116
function (encodedVal) {
99117
return options.decoder(encodedVal, defaults.decoder, charset, 'value');
100118
}
@@ -121,7 +139,13 @@ var parseValues = function parseQueryStringValues(str, options) {
121139
};
122140

123141
var parseObject = function (chain, val, options, valuesParsed) {
124-
var leaf = valuesParsed ? val : parseArrayValue(val, options);
142+
var currentArrayLength = 0;
143+
if (chain.length > 0 && chain[chain.length - 1] === '[]') {
144+
var parentKey = chain.slice(0, -1).join('');
145+
currentArrayLength = Array.isArray(val) && val[parentKey] ? val[parentKey].length : 0;
146+
}
147+
148+
var leaf = valuesParsed ? val : parseArrayValue(val, options, currentArrayLength);
125149

126150
for (var i = chain.length - 1; i >= 0; --i) {
127151
var obj;
@@ -235,6 +259,11 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
235259
if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
236260
throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
237261
}
262+
263+
if (typeof opts.throwOnLimitExceeded !== 'undefined' && typeof opts.throwOnLimitExceeded !== 'boolean') {
264+
throw new TypeError('`throwOnLimitExceeded` option must be a boolean');
265+
}
266+
238267
var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset;
239268

240269
var duplicates = typeof opts.duplicates === 'undefined' ? defaults.duplicates : opts.duplicates;
@@ -266,7 +295,8 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
266295
parseArrays: opts.parseArrays !== false,
267296
plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
268297
strictDepth: typeof opts.strictDepth === 'boolean' ? !!opts.strictDepth : defaults.strictDepth,
269-
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
298+
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling,
299+
throwOnLimitExceeded: typeof opts.throwOnLimitExceeded === 'boolean' ? opts.throwOnLimitExceeded : false
270300
};
271301
};
272302

‎test/parse.js

+103-14
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ test('parse()', function (t) {
118118
st.end();
119119
});
120120

121-
t.test('should decode dot in key of object, and allow enabling dot notation when decodeDotInKeys is set to true and allowDots is undefined', function (st) {
121+
t.test('decodes dot in key of object, and allow enabling dot notation when decodeDotInKeys is set to true and allowDots is undefined', function (st) {
122122
st.deepEqual(
123123
qs.parse(
124124
'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
@@ -131,7 +131,7 @@ test('parse()', function (t) {
131131
st.end();
132132
});
133133

134-
t.test('should throw when decodeDotInKeys is not of type boolean', function (st) {
134+
t.test('throws when decodeDotInKeys is not of type boolean', function (st) {
135135
st['throws'](
136136
function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: 'foobar' }); },
137137
TypeError
@@ -161,7 +161,7 @@ test('parse()', function (t) {
161161
st.end();
162162
});
163163

164-
t.test('should throw when allowEmptyArrays is not of type boolean', function (st) {
164+
t.test('throws when allowEmptyArrays is not of type boolean', function (st) {
165165
st['throws'](
166166
function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: 'foobar' }); },
167167
TypeError
@@ -444,7 +444,7 @@ test('parse()', function (t) {
444444
st.end();
445445
});
446446

447-
t.test('should not throw when a native prototype has an enumerable property', function (st) {
447+
t.test('does not throw when a native prototype has an enumerable property', function (st) {
448448
st.intercept(Object.prototype, 'crash', { value: '' });
449449
st.intercept(Array.prototype, 'crash', { value: '' });
450450

@@ -965,7 +965,7 @@ test('parse()', function (t) {
965965
st.end();
966966
});
967967

968-
t.test('should ignore an utf8 sentinel with an unknown value', function (st) {
968+
t.test('ignores an utf8 sentinel with an unknown value', function (st) {
969969
st.deepEqual(qs.parse('utf8=foo&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'utf-8' }), { ø: 'ø' });
970970
st.end();
971971
});
@@ -1035,6 +1035,95 @@ test('parse()', function (t) {
10351035
st.end();
10361036
});
10371037

1038+
t.test('parameter limit tests', function (st) {
1039+
st.test('does not throw error when within parameter limit', function (sst) {
1040+
var result = qs.parse('a=1&b=2&c=3', { parameterLimit: 5, throwOnLimitExceeded: true });
1041+
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses without errors');
1042+
sst.end();
1043+
});
1044+
1045+
st.test('throws error when throwOnLimitExceeded is present but not boolean', function (sst) {
1046+
sst['throws'](
1047+
function () {
1048+
qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: 'true' });
1049+
},
1050+
new TypeError('`throwOnLimitExceeded` option must be a boolean'),
1051+
'throws error when throwOnLimitExceeded is present and not boolean'
1052+
);
1053+
sst.end();
1054+
});
1055+
1056+
st.test('throws error when parameter limit exceeded', function (sst) {
1057+
sst['throws'](
1058+
function () {
1059+
qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: true });
1060+
},
1061+
new RangeError('Parameter limit exceeded. Only 3 parameters allowed.'),
1062+
'throws error when parameter limit is exceeded'
1063+
);
1064+
sst.end();
1065+
});
1066+
1067+
st.test('silently truncates when throwOnLimitExceeded is not given', function (sst) {
1068+
var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3 });
1069+
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
1070+
sst.end();
1071+
});
1072+
1073+
st.test('silently truncates when parameter limit exceeded without error', function (sst) {
1074+
var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3, throwOnLimitExceeded: false });
1075+
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
1076+
sst.end();
1077+
});
1078+
1079+
st.test('allows unlimited parameters when parameterLimit set to Infinity', function (sst) {
1080+
var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity });
1081+
sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation');
1082+
sst.end();
1083+
});
1084+
1085+
st.end();
1086+
});
1087+
1088+
t.test('array limit tests', function (st) {
1089+
st.test('does not throw error when array is within limit', function (sst) {
1090+
var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 5, throwOnLimitExceeded: true });
1091+
sst.deepEqual(result, { a: ['1', '2', '3'] }, 'parses array without errors');
1092+
sst.end();
1093+
});
1094+
1095+
st.test('throws error when throwOnLimitExceeded is present but not boolean for array limit', function (sst) {
1096+
sst['throws'](
1097+
function () {
1098+
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: 'true' });
1099+
},
1100+
new TypeError('`throwOnLimitExceeded` option must be a boolean'),
1101+
'throws error when throwOnLimitExceeded is present and not boolean for array limit'
1102+
);
1103+
sst.end();
1104+
});
1105+
1106+
st.test('throws error when array limit exceeded', function (sst) {
1107+
sst['throws'](
1108+
function () {
1109+
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: true });
1110+
},
1111+
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
1112+
'throws error when array limit is exceeded'
1113+
);
1114+
sst.end();
1115+
});
1116+
1117+
st.test('converts array to object if length is greater than limit', function (sst) {
1118+
var result = qs.parse('a[1]=1&a[2]=2&a[3]=3&a[4]=4&a[5]=5&a[6]=6', { arrayLimit: 5 });
1119+
1120+
sst.deepEqual(result, { a: { 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6' } }, 'parses into object if array length is greater than limit');
1121+
sst.end();
1122+
});
1123+
1124+
st.end();
1125+
});
1126+
10381127
t.end();
10391128
});
10401129

@@ -1093,7 +1182,7 @@ test('qs strictDepth option - throw cases', function (t) {
10931182
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
10941183
},
10951184
RangeError,
1096-
'Should throw RangeError'
1185+
'throws RangeError'
10971186
);
10981187
st.end();
10991188
});
@@ -1104,7 +1193,7 @@ test('qs strictDepth option - throw cases', function (t) {
11041193
qs.parse('a[0][1][2][3][4]=b', { depth: 3, strictDepth: true });
11051194
},
11061195
RangeError,
1107-
'Should throw RangeError'
1196+
'throws RangeError'
11081197
);
11091198
st.end();
11101199
});
@@ -1115,7 +1204,7 @@ test('qs strictDepth option - throw cases', function (t) {
11151204
qs.parse('a[b][c][0][d][e]=f', { depth: 3, strictDepth: true });
11161205
},
11171206
RangeError,
1118-
'Should throw RangeError'
1207+
'throws RangeError'
11191208
);
11201209
st.end();
11211210
});
@@ -1126,7 +1215,7 @@ test('qs strictDepth option - throw cases', function (t) {
11261215
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 3, strictDepth: true });
11271216
},
11281217
RangeError,
1129-
'Should throw RangeError'
1218+
'throws RangeError'
11301219
);
11311220
st.end();
11321221
});
@@ -1140,7 +1229,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
11401229
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 0, strictDepth: true });
11411230
},
11421231
RangeError,
1143-
'Should not throw RangeError'
1232+
'does not throw RangeError'
11441233
);
11451234
st.end();
11461235
});
@@ -1149,7 +1238,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
11491238
st.doesNotThrow(
11501239
function () {
11511240
var result = qs.parse('a[b]=c', { depth: 1, strictDepth: true });
1152-
st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly');
1241+
st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
11531242
}
11541243
);
11551244
st.end();
@@ -1159,7 +1248,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
11591248
st.doesNotThrow(
11601249
function () {
11611250
var result = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
1162-
st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'Should parse with depth limit');
1251+
st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'parses with depth limit');
11631252
}
11641253
);
11651254
st.end();
@@ -1169,7 +1258,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
11691258
st.doesNotThrow(
11701259
function () {
11711260
var result = qs.parse('a[b]=c', { depth: 1 });
1172-
st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly');
1261+
st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
11731262
}
11741263
);
11751264
st.end();
@@ -1179,7 +1268,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
11791268
st.doesNotThrow(
11801269
function () {
11811270
var result = qs.parse('a[b][c]=d', { depth: 2, strictDepth: true });
1182-
st.deepEqual(result, { a: { b: { c: 'd' } } }, 'Should parse correctly');
1271+
st.deepEqual(result, { a: { b: { c: 'd' } } }, 'parses correctly');
11831272
}
11841273
);
11851274
st.end();

0 commit comments

Comments
 (0)
Please sign in to comment.