Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New]: parse: add throwOnLimitExceeded option #517

Merged
merged 1 commit into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
assert.deepEqual(limited, { a: 'b' });
```

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.
```javascript
try {
qs.parse('a=1&b=2&c=3&d=4', { parameterLimit: 3, throwOnLimitExceeded: true });
} catch (err) {
assert(err instanceof Error);
assert.strictEqual(err.message, 'Parameter limit exceeded. Only 3 parameters allowed.');
}
```

When `throwOnLimitExceeded` is set to `false` (default), **qs** will parse up to the specified `parameterLimit` and ignore the rest without throwing an error.
ljharb marked this conversation as resolved.
Show resolved Hide resolved

To bypass the leading question mark, use `ignoreQueryPrefix`:

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

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.
ljharb marked this conversation as resolved.
Show resolved Hide resolved
```javascript
try {
qs.parse('a[1]=b', { arrayLimit: 0, throwOnLimitExceeded: true });
} catch (err) {
assert(err instanceof Error);
assert.strictEqual(err.message, 'Array limit exceeded. Only 0 elements allowed in an array.');
}
```

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

To disable array parsing entirely, set `parseArrays` to `false`.

```javascript
Expand Down
40 changes: 35 additions & 5 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ var interpretNumericEntities = function (str) {
});
};

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

if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}

return val;
};

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

var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
cleanStr = cleanStr.replace(/%5B/gi, '[').replace(/%5D/gi, ']');

var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit;
var parts = cleanStr.split(options.delimiter, limit);
var parts = cleanStr.split(
options.delimiter,
options.throwOnLimitExceeded ? limit + 1 : limit
);

if (options.throwOnLimitExceeded && parts.length > limit) {
throw new RangeError('Parameter limit exceeded. Only ' + limit + ' parameter' + (limit === 1 ? '' : 's') + ' allowed.');
}

var skipIndex = -1; // Keep track of where the utf8 sentinel was found
var i;

Expand Down Expand Up @@ -93,8 +106,13 @@ var parseValues = function parseQueryStringValues(str, options) {
val = options.strictNullHandling ? null : '';
} else {
key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');

val = utils.maybeMap(
parseArrayValue(part.slice(pos + 1), options),
parseArrayValue(
part.slice(pos + 1),
options,
isArray(obj[key]) ? obj[key].length : 0
),
function (encodedVal) {
return options.decoder(encodedVal, defaults.decoder, charset, 'value');
}
Expand All @@ -121,7 +139,13 @@ var parseValues = function parseQueryStringValues(str, options) {
};

var parseObject = function (chain, val, options, valuesParsed) {
var leaf = valuesParsed ? val : parseArrayValue(val, options);
var currentArrayLength = 0;
if (chain.length > 0 && chain[chain.length - 1] === '[]') {
var parentKey = chain.slice(0, -1).join('');
currentArrayLength = Array.isArray(val) && val[parentKey] ? val[parentKey].length : 0;
}

var leaf = valuesParsed ? val : parseArrayValue(val, options, currentArrayLength);

for (var i = chain.length - 1; i >= 0; --i) {
var obj;
Expand Down Expand Up @@ -235,6 +259,11 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
}

if (typeof opts.throwOnLimitExceeded !== 'undefined' && typeof opts.throwOnLimitExceeded !== 'boolean') {
throw new TypeError('`throwOnLimitExceeded` option must be a boolean');
}

var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset;

var duplicates = typeof opts.duplicates === 'undefined' ? defaults.duplicates : opts.duplicates;
Expand Down Expand Up @@ -266,7 +295,8 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
parseArrays: opts.parseArrays !== false,
plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
strictDepth: typeof opts.strictDepth === 'boolean' ? !!opts.strictDepth : defaults.strictDepth,
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling,
throwOnLimitExceeded: typeof opts.throwOnLimitExceeded === 'boolean' ? opts.throwOnLimitExceeded : false
};
};

Expand Down
117 changes: 103 additions & 14 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ test('parse()', function (t) {
st.end();
});

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) {
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) {
st.deepEqual(
qs.parse(
'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
Expand All @@ -131,7 +131,7 @@ test('parse()', function (t) {
st.end();
});

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

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

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

Expand Down Expand Up @@ -965,7 +965,7 @@ test('parse()', function (t) {
st.end();
});

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

t.test('parameter limit tests', function (st) {
st.test('does not throw error when within parameter limit', function (sst) {
var result = qs.parse('a=1&b=2&c=3', { parameterLimit: 5, throwOnLimitExceeded: true });
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses without errors');
sst.end();
});

st.test('throws error when throwOnLimitExceeded is present but not boolean', function (sst) {
sst['throws'](
function () {
qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: 'true' });
},
new TypeError('`throwOnLimitExceeded` option must be a boolean'),
'throws error when throwOnLimitExceeded is present and not boolean'
);
sst.end();
});

st.test('throws error when parameter limit exceeded', function (sst) {
sst['throws'](
function () {
qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Parameter limit exceeded. Only 3 parameters allowed.'),
'throws error when parameter limit is exceeded'
);
sst.end();
});

st.test('silently truncates when throwOnLimitExceeded is not given', function (sst) {
var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3 });
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
sst.end();
});

st.test('silently truncates when parameter limit exceeded without error', function (sst) {
var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3, throwOnLimitExceeded: false });
sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
sst.end();
});

st.test('allows unlimited parameters when parameterLimit set to Infinity', function (sst) {
var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity });
sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation');
sst.end();
});

st.end();
});

t.test('array limit tests', function (st) {
st.test('does not throw error when array is within limit', function (sst) {
var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 5, throwOnLimitExceeded: true });
sst.deepEqual(result, { a: ['1', '2', '3'] }, 'parses array without errors');
sst.end();
});

st.test('throws error when throwOnLimitExceeded is present but not boolean for array limit', function (sst) {
sst['throws'](
function () {
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: 'true' });
},
new TypeError('`throwOnLimitExceeded` option must be a boolean'),
'throws error when throwOnLimitExceeded is present and not boolean for array limit'
);
sst.end();
});

st.test('throws error when array limit exceeded', function (sst) {
sst['throws'](
function () {
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: true });
},
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
'throws error when array limit is exceeded'
);
sst.end();
});

st.test('converts array to object if length is greater than limit', function (sst) {
var result = qs.parse('a[1]=1&a[2]=2&a[3]=3&a[4]=4&a[5]=5&a[6]=6', { arrayLimit: 5 });

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');
sst.end();
});

st.end();
});

t.end();
});

Expand Down Expand Up @@ -1093,7 +1182,7 @@ test('qs strictDepth option - throw cases', function (t) {
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
},
RangeError,
'Should throw RangeError'
'throws RangeError'
);
st.end();
});
Expand All @@ -1104,7 +1193,7 @@ test('qs strictDepth option - throw cases', function (t) {
qs.parse('a[0][1][2][3][4]=b', { depth: 3, strictDepth: true });
},
RangeError,
'Should throw RangeError'
'throws RangeError'
);
st.end();
});
Expand All @@ -1115,7 +1204,7 @@ test('qs strictDepth option - throw cases', function (t) {
qs.parse('a[b][c][0][d][e]=f', { depth: 3, strictDepth: true });
},
RangeError,
'Should throw RangeError'
'throws RangeError'
);
st.end();
});
Expand All @@ -1126,7 +1215,7 @@ test('qs strictDepth option - throw cases', function (t) {
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 3, strictDepth: true });
},
RangeError,
'Should throw RangeError'
'throws RangeError'
);
st.end();
});
Expand All @@ -1140,7 +1229,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 0, strictDepth: true });
},
RangeError,
'Should not throw RangeError'
'does not throw RangeError'
);
st.end();
});
Expand All @@ -1149,7 +1238,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b]=c', { depth: 1, strictDepth: true });
st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly');
st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
}
);
st.end();
Expand All @@ -1159,7 +1248,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'Should parse with depth limit');
st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'parses with depth limit');
}
);
st.end();
Expand All @@ -1169,7 +1258,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b]=c', { depth: 1 });
st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly');
st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
}
);
st.end();
Expand All @@ -1179,7 +1268,7 @@ test('qs strictDepth option - non-throw cases', function (t) {
st.doesNotThrow(
function () {
var result = qs.parse('a[b][c]=d', { depth: 2, strictDepth: true });
st.deepEqual(result, { a: { b: { c: 'd' } } }, 'Should parse correctly');
st.deepEqual(result, { a: { b: { c: 'd' } } }, 'parses correctly');
}
);
st.end();
Expand Down
Loading