Skip to content

[Fix] stringify: encode keys when allowEmptyArrays is set#561

Draft
DucMinhNe wants to merge 1 commit into
ljharb:mainfrom
DucMinhNe:fix/allow-empty-arrays-encode
Draft

[Fix] stringify: encode keys when allowEmptyArrays is set#561
DucMinhNe wants to merge 1 commit into
ljharb:mainfrom
DucMinhNe:fix/allow-empty-arrays-encode

Conversation

@DucMinhNe

Copy link
Copy Markdown

When allowEmptyArrays: true is set, an empty-array key is emitted via an early return that concatenates the raw prefix with [], bypassing the encoder and formatter that every other key path runs through. As a result keys that require encoding are emitted unencoded (and invalid):

qs.stringify({ 'a b': [] }, { allowEmptyArrays: true });
// before: 'a b[]'   (raw space — invalid)
// after:  'a%20b[]'

This is the same class of bug as #554 (strictNullHandling not applying the formatter to the encoded key, sibling code branch just above). The fix mirrors that one: wrap the prefix in formatter(encoder(prefix, defaults.encoder, charset, 'key', format)) when an encoder is present and we're not in encodeValuesOnly mode — matching the existing strictNullHandling line exactly.

Behavior

  • default (RFC3986): { 'a b': [] }a%20b[]
  • format: 'RFC1738': → a+b[]
  • encodeValuesOnly: true: → a b[] (key left raw, as expected)
  • encode: false: → a b[] (unchanged)
  • keys that need no encoding (e.g. foo) are unchanged, so existing tests/README stay valid.

Tests

Added a regression test (should encode the key of an empty array when allowEmptyArrays is set) covering the default, RFC1738, encodeValuesOnly, and encode: false cases. Full suite green (941 tests).

@ljharb ljharb left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good - just a few more things before merging.

Comment thread lib/stringify.js

if (allowEmptyArrays && isArray(obj) && obj.length === 0) {
return adjustedPrefix + '[]';
return (encoder && !encodeValuesOnly ? formatter(encoder(adjustedPrefix, defaults.encoder, charset, 'key', format)) : adjustedPrefix) + '[]';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix. One small thing: the commit message says this mirrors the strictNullHandling line, but that sibling (the #554 fix just above) wraps formatter() around the whole conditional, including the raw branch. Here formatter is only on the encoded branch.

They are byte-identical for every real input, but they diverge for a key whose literal text contains a character the RFC1738 formatter rewrites (e.g. a literal %20) on the encodeValuesOnly / encode: false path: this emits a%20b[], while the normal non-empty-array path emits a+b... for the same key. Wrapping the whole conditional matches #554 exactly, stays byte-identical for every real case, and fixes that edge. Verified: 1006 tests pass and lib/stringify.js stays at 100% coverage.

Suggested change
return (encoder && !encodeValuesOnly ? formatter(encoder(adjustedPrefix, defaults.encoder, charset, 'key', format)) : adjustedPrefix) + '[]';
return formatter(encoder && !encodeValuesOnly ? encoder(adjustedPrefix, defaults.encoder, charset, 'key', format) : adjustedPrefix) + '[]';

Comment thread test/stringify.js
);
st.equal(
qs.stringify({ 'a b': [] }, { allowEmptyArrays: true, encodeValuesOnly: true }),
'a b[]',

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These encodeValuesOnly / encode: false cases assert a b[], which is byte-identical to the old buggy output, so the test can't distinguish "intentionally unencoded" from the bug. Worth making the encoded path's fidelity explicit, and adding a consistency check against the same key with a real value (that is the assertion that surfaces the formatter nuance in lib). For example, before st.end() (an empty array round-trips to [''], so assert on the key, not a full deep-equal):

st.ok(
    'a b' in qs.parse(qs.stringify({ 'a b': [] }, { allowEmptyArrays: true }), { allowEmptyArrays: true }),
    'the encoded key round-trips back to the original'
);
st.equal(
    qs.stringify({ 'a b': ['x'] }, { allowEmptyArrays: true, arrayFormat: 'brackets' }),
    'a%20b%5B%5D=x',
    'the same key with a value encodes the key prefix identically'
);

@ljharb ljharb marked this pull request as draft June 23, 2026 06:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants