Skip to content

Commit 21f80b3

Browse files
committed
[Fix] stringify: skip null/undefined entries in arrayFormat: 'comma' + encodeValuesOnly instead of crashing in encoder
When `arrayFormat: 'comma'` and `encodeValuesOnly: true` were both set, the comma branch passed array entries directly to the encoder via `utils.maybeMap` before the per-element null handling in the main loop. A `null` or `undefined` entry reached `utils.encode` as the `str` argument and `str.length` threw `TypeError`. Neither `skipNulls` nor `strictNullHandling` prevented the crash — both are checked downstream of the encode call on line 145, inside the per-`objKeys` loop, but the comma path collapses the entire array into a single `objKeys` entry before the loop runs. The fix wraps the encoder in a closure that passes `null`/`undefined` through unchanged, so they reach the `join(',')` step as-is. This matches the non-`encodeValuesOnly` comma path, which already joins before encoding and produces `a=%2Cb` for `{ a: [null, 'b'] }`; under `encodeValuesOnly` the comma stays unencoded as a separator, yielding `a=,b`. Single-null arrays still collapse to `null` via the existing `obj.join(',') || null` and remain subject to `skipNulls` / `strictNullHandling` in the main loop. Same class of issue as the filter-array path fixed in 0c180a4, introduced in 04eac8d (2021-04-19) when the comma + `encodeValuesOnly` branch was first added.
1 parent a0a81ea commit 21f80b3

3 files changed

Lines changed: 47 additions & 1 deletion

File tree

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default [
1111
rules: {
1212
complexity: 'off',
1313
'consistent-return': 'warn',
14+
eqeqeq: ['error', 'allow-null'],
1415
'func-name-matching': 'off',
1516
'id-length': [
1617
'error',

lib/stringify.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ var stringify = function stringify(
142142
if (generateArrayPrefix === 'comma' && isArray(obj)) {
143143
// we need to join elements in
144144
if (encodeValuesOnly && encoder) {
145-
obj = utils.maybeMap(obj, encoder);
145+
obj = utils.maybeMap(obj, function (v) {
146+
return v == null ? v : encoder(v);
147+
});
146148
}
147149
objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
148150
} else if (isArray(filter)) {

test/stringify.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,49 @@ test('stringify()', function (t) {
651651
st.end();
652652
});
653653

654+
t.test('does not crash on null/undefined entries in arrayFormat=comma with encodeValuesOnly', function (st) {
655+
st.doesNotThrow(
656+
function () { qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
657+
'does not pass a raw null array entry to the encoder'
658+
);
659+
st.doesNotThrow(
660+
function () { qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
661+
'does not pass a raw undefined array entry to the encoder'
662+
);
663+
st.doesNotThrow(
664+
function () { qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
665+
'does not crash on a single-null array'
666+
);
667+
668+
st.equal(
669+
qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
670+
'a=,b',
671+
'null entry joins as empty, comma stays unencoded under encodeValuesOnly'
672+
);
673+
st.equal(
674+
qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
675+
'a=,b',
676+
'undefined entry joins as empty, comma stays unencoded under encodeValuesOnly'
677+
);
678+
st.equal(
679+
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
680+
'a=',
681+
'single-null array stringifies as empty value'
682+
);
683+
st.equal(
684+
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, strictNullHandling: true }),
685+
'a',
686+
'strictNullHandling drops the equals sign for a single-null array'
687+
);
688+
st.equal(
689+
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, skipNulls: true }),
690+
'',
691+
'skipNulls drops a single-null array entirely'
692+
);
693+
694+
st.end();
695+
});
696+
654697
t.test('stringifies a null object', { skip: !hasProto }, function (st) {
655698
st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
656699
st.end();

0 commit comments

Comments
 (0)