Skip to content

Commit 188d5f2

Browse files
committed
Drop empty and whitespace-only keys in parse() and stringify()
Fixes #295
1 parent b46abfc commit 188d5f2

File tree

4 files changed

+108
-1
lines changed

4 files changed

+108
-1
lines changed

base.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,11 @@ export type ParsedQuery<T = string> = Record<string, T | null | Array<T | null>>
301301
/**
302302
Parse a query string into an object. Leading `?` or `#` are ignored, so you can pass `location.search` or `location.hash` directly.
303303
304+
@param query - The query string to parse.
305+
304306
The returned object is created with [`Object.create(null)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) and thus does not have a `prototype`.
305307
306-
@param query - The query string to parse.
308+
Note: Keys that are empty or contain only whitespace are dropped from the result.
307309
*/
308310
export function parse(query: string, options: {parseBooleans: true; parseNumbers: true} & ParseOptions): ParsedQuery<string | boolean | number>;
309311
export function parse(query: string, options: {parseBooleans: true} & ParseOptions): ParsedQuery<string | boolean>;
@@ -609,6 +611,8 @@ import queryString from 'query-string';
609611
queryString.stringify({foo: 'bar', baz: 42, qux: true});
610612
//=> 'baz=42&foo=bar&qux=true'
611613
```
614+
615+
Note: Keys that are empty or contain only whitespace are ignored and will not appear in the output.
612616
*/
613617
export function stringify(
614618
// TODO: Use the below instead when the following TS issues are fixed:

base.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ function parserForArrayFormat(options) {
147147

148148
key = key.replace(/\[\d*]$/, '');
149149

150+
// Skip empty or whitespace-only keys
151+
if (key.trim() === '') {
152+
return;
153+
}
154+
150155
if (!result) {
151156
accumulator[key] = value;
152157
return;
@@ -165,6 +170,11 @@ function parserForArrayFormat(options) {
165170
result = /(\[])$/.exec(key);
166171
key = key.replace(/\[]$/, '');
167172

173+
// Skip empty or whitespace-only keys
174+
if (key.trim() === '') {
175+
return;
176+
}
177+
168178
if (!result) {
169179
accumulator[key] = value;
170180
return;
@@ -184,6 +194,11 @@ function parserForArrayFormat(options) {
184194
result = /(:list)$/.exec(key);
185195
key = key.replace(/:list$/, '');
186196

197+
// Skip empty or whitespace-only keys
198+
if (key.trim() === '') {
199+
return;
200+
}
201+
187202
if (!result) {
188203
accumulator[key] = value;
189204
return;
@@ -201,6 +216,11 @@ function parserForArrayFormat(options) {
201216
case 'comma':
202217
case 'separator': {
203218
return (key, value, accumulator) => {
219+
// Skip empty or whitespace-only keys
220+
if (key.trim() === '') {
221+
return;
222+
}
223+
204224
const isArray = typeof value === 'string' && value.includes(options.arrayFormatSeparator);
205225
const newValue = isArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : (value === null ? value : decode(value, options));
206226
accumulator[key] = newValue;
@@ -212,6 +232,11 @@ function parserForArrayFormat(options) {
212232
const isArray = /(\[])$/.test(key);
213233
key = key.replace(/\[]$/, '');
214234

235+
// Skip empty or whitespace-only keys
236+
if (key.trim() === '') {
237+
return;
238+
}
239+
215240
if (!isArray) {
216241
accumulator[key] = value ? decode(value, options) : value;
217242
return;
@@ -232,6 +257,11 @@ function parserForArrayFormat(options) {
232257

233258
default: {
234259
return (key, value, accumulator) => {
260+
// Skip empty or whitespace-only keys
261+
if (key.trim() === '') {
262+
return;
263+
}
264+
235265
if (accumulator[key] === undefined) {
236266
accumulator[key] = value;
237267
return;
@@ -463,6 +493,11 @@ export function stringify(object, options) {
463493
}
464494

465495
return keys.map(key => {
496+
// Skip empty or whitespace-only keys
497+
if (key.trim() === '') {
498+
return '';
499+
}
500+
466501
let value = object[key];
467502

468503
// Apply replacer function if provided

readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ Parse a query string into an object. Leading `?` or `#` are ignored, so you can
5252

5353
The returned object is created with [`Object.create(null)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) and thus does not have a `prototype`.
5454

55+
> [!NOTE]
56+
> Keys that are empty or contain only whitespace are dropped from the result.
57+
5558
```js
5659
queryString.parse('?foo=bar');
5760
//=> {foo: 'bar'}
@@ -328,6 +331,9 @@ Stringify an object into a query string and sorting the keys.
328331
329332
**Supported value types:** `string`, `number`, `bigint`, `boolean`, `null`, `undefined`, and arrays of these types. Other types like `Symbol`, functions, or objects (except arrays) will throw an error.
330333
334+
> [!NOTE]
335+
> Keys that are empty or contain only whitespace are ignored and will not appear in the output.
336+
331337
#### options
332338
333339
Type: `object`

test-issue-295.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import test from 'ava';
2+
import queryString from './index.js';
3+
4+
test('parse() drops empty and whitespace-only keys', t => {
5+
// Original issue - encoded whitespace key
6+
t.deepEqual(queryString.parse('?%20&'), {});
7+
8+
// Various whitespace encodings
9+
t.deepEqual(queryString.parse('?%20'), {});
10+
t.deepEqual(queryString.parse('?%09'), {}); // Tab
11+
t.deepEqual(queryString.parse('?+'), {}); // Plus as space
12+
13+
// Empty keys
14+
t.deepEqual(queryString.parse('?&&'), {});
15+
16+
// Mixed valid and invalid keys
17+
t.deepEqual(queryString.parse('?valid=1&%20&another=2'), {
18+
valid: '1',
19+
another: '2',
20+
});
21+
22+
// Valid keys are preserved
23+
t.deepEqual(queryString.parse('?a'), {a: null});
24+
t.deepEqual(queryString.parse('?a='), {a: ''});
25+
});
26+
27+
test('stringify() ignores empty and whitespace keys', t => {
28+
// Empty and whitespace keys
29+
t.is(queryString.stringify({'': 'value'}), '');
30+
t.is(queryString.stringify({' ': 'value'}), '');
31+
t.is(queryString.stringify({'\t': 'value'}), '');
32+
33+
// Mixed valid and invalid
34+
t.is(queryString.stringify({valid: '1', '': 'ignored'}), 'valid=1');
35+
36+
// Valid keys work normally
37+
t.is(queryString.stringify({a: null}), 'a');
38+
t.is(queryString.stringify({a: ''}), 'a=');
39+
});
40+
41+
test('symmetry: parse and stringify round-trip', t => {
42+
// Original issue case
43+
t.is(queryString.stringify(queryString.parse('?%20&')), '');
44+
45+
// Empty keys
46+
t.is(queryString.stringify(queryString.parse('?&&')), '');
47+
48+
// Mixed keys maintain valid ones
49+
t.is(queryString.stringify(queryString.parse('?valid=1&%20&')), 'valid=1');
50+
});
51+
52+
test('array formats handle empty keys correctly', t => {
53+
// Parse with different array formats
54+
t.deepEqual(queryString.parse('?%20[]=1', {arrayFormat: 'bracket'}), {});
55+
t.deepEqual(queryString.parse('?%20[0]=1', {arrayFormat: 'index'}), {});
56+
t.deepEqual(queryString.parse('?%20=1,2', {arrayFormat: 'comma'}), {});
57+
58+
// Stringify with different array formats
59+
t.is(queryString.stringify({'': ['1']}, {arrayFormat: 'bracket'}), '');
60+
t.is(queryString.stringify({' ': ['1']}, {arrayFormat: 'index'}), '');
61+
t.is(queryString.stringify({'': ['1', '2']}, {arrayFormat: 'comma'}), '');
62+
});

0 commit comments

Comments
 (0)