diff --git a/base.d.ts b/base.d.ts index c6415dd..15a82ef 100644 --- a/base.d.ts +++ b/base.d.ts @@ -301,9 +301,11 @@ export type ParsedQuery = Record> /** Parse a query string into an object. Leading `?` or `#` are ignored, so you can pass `location.search` or `location.hash` directly. +@param query - The query string to parse. + 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`. -@param query - The query string to parse. +Note: Keys that are empty or contain only whitespace are dropped from the result. */ export function parse(query: string, options: {parseBooleans: true; parseNumbers: true} & ParseOptions): ParsedQuery; export function parse(query: string, options: {parseBooleans: true} & ParseOptions): ParsedQuery; @@ -609,6 +611,8 @@ import queryString from 'query-string'; queryString.stringify({foo: 'bar', baz: 42, qux: true}); //=> 'baz=42&foo=bar&qux=true' ``` + +Note: Keys that are empty or contain only whitespace are ignored and will not appear in the output. */ export function stringify( // TODO: Use the below instead when the following TS issues are fixed: diff --git a/base.js b/base.js index 9a0a73f..009807c 100644 --- a/base.js +++ b/base.js @@ -147,6 +147,11 @@ function parserForArrayFormat(options) { key = key.replace(/\[\d*]$/, ''); + // Skip empty or whitespace-only keys + if (key.trim() === '') { + return; + } + if (!result) { accumulator[key] = value; return; @@ -165,6 +170,11 @@ function parserForArrayFormat(options) { result = /(\[])$/.exec(key); key = key.replace(/\[]$/, ''); + // Skip empty or whitespace-only keys + if (key.trim() === '') { + return; + } + if (!result) { accumulator[key] = value; return; @@ -184,6 +194,11 @@ function parserForArrayFormat(options) { result = /(:list)$/.exec(key); key = key.replace(/:list$/, ''); + // Skip empty or whitespace-only keys + if (key.trim() === '') { + return; + } + if (!result) { accumulator[key] = value; return; @@ -201,6 +216,11 @@ function parserForArrayFormat(options) { case 'comma': case 'separator': { return (key, value, accumulator) => { + // Skip empty or whitespace-only keys + if (key.trim() === '') { + return; + } + const isArray = typeof value === 'string' && value.includes(options.arrayFormatSeparator); const newValue = isArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : (value === null ? value : decode(value, options)); accumulator[key] = newValue; @@ -212,6 +232,11 @@ function parserForArrayFormat(options) { const isArray = /(\[])$/.test(key); key = key.replace(/\[]$/, ''); + // Skip empty or whitespace-only keys + if (key.trim() === '') { + return; + } + if (!isArray) { accumulator[key] = value ? decode(value, options) : value; return; @@ -232,6 +257,11 @@ function parserForArrayFormat(options) { default: { return (key, value, accumulator) => { + // Skip empty or whitespace-only keys + if (key.trim() === '') { + return; + } + if (accumulator[key] === undefined) { accumulator[key] = value; return; @@ -463,6 +493,11 @@ export function stringify(object, options) { } return keys.map(key => { + // Skip empty or whitespace-only keys + if (key.trim() === '') { + return ''; + } + let value = object[key]; // Apply replacer function if provided diff --git a/readme.md b/readme.md index 22375e1..37049f2 100644 --- a/readme.md +++ b/readme.md @@ -52,6 +52,9 @@ Parse a query string into an object. Leading `?` or `#` are ignored, so you can 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`. +> [!NOTE] +> Keys that are empty or contain only whitespace are dropped from the result. + ```js queryString.parse('?foo=bar'); //=> {foo: 'bar'} @@ -328,6 +331,9 @@ Stringify an object into a query string and sorting the keys. **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. +> [!NOTE] +> Keys that are empty or contain only whitespace are ignored and will not appear in the output. + #### options Type: `object` diff --git a/test-issue-295.js b/test-issue-295.js new file mode 100644 index 0000000..793b768 --- /dev/null +++ b/test-issue-295.js @@ -0,0 +1,62 @@ +import test from 'ava'; +import queryString from './index.js'; + +test('parse() drops empty and whitespace-only keys', t => { + // Original issue - encoded whitespace key + t.deepEqual(queryString.parse('?%20&'), {}); + + // Various whitespace encodings + t.deepEqual(queryString.parse('?%20'), {}); + t.deepEqual(queryString.parse('?%09'), {}); // Tab + t.deepEqual(queryString.parse('?+'), {}); // Plus as space + + // Empty keys + t.deepEqual(queryString.parse('?&&'), {}); + + // Mixed valid and invalid keys + t.deepEqual(queryString.parse('?valid=1&%20&another=2'), { + valid: '1', + another: '2', + }); + + // Valid keys are preserved + t.deepEqual(queryString.parse('?a'), {a: null}); + t.deepEqual(queryString.parse('?a='), {a: ''}); +}); + +test('stringify() ignores empty and whitespace keys', t => { + // Empty and whitespace keys + t.is(queryString.stringify({'': 'value'}), ''); + t.is(queryString.stringify({' ': 'value'}), ''); + t.is(queryString.stringify({'\t': 'value'}), ''); + + // Mixed valid and invalid + t.is(queryString.stringify({valid: '1', '': 'ignored'}), 'valid=1'); + + // Valid keys work normally + t.is(queryString.stringify({a: null}), 'a'); + t.is(queryString.stringify({a: ''}), 'a='); +}); + +test('symmetry: parse and stringify round-trip', t => { + // Original issue case + t.is(queryString.stringify(queryString.parse('?%20&')), ''); + + // Empty keys + t.is(queryString.stringify(queryString.parse('?&&')), ''); + + // Mixed keys maintain valid ones + t.is(queryString.stringify(queryString.parse('?valid=1&%20&')), 'valid=1'); +}); + +test('array formats handle empty keys correctly', t => { + // Parse with different array formats + t.deepEqual(queryString.parse('?%20[]=1', {arrayFormat: 'bracket'}), {}); + t.deepEqual(queryString.parse('?%20[0]=1', {arrayFormat: 'index'}), {}); + t.deepEqual(queryString.parse('?%20=1,2', {arrayFormat: 'comma'}), {}); + + // Stringify with different array formats + t.is(queryString.stringify({'': ['1']}, {arrayFormat: 'bracket'}), ''); + t.is(queryString.stringify({' ': ['1']}, {arrayFormat: 'index'}), ''); + t.is(queryString.stringify({'': ['1', '2']}, {arrayFormat: 'comma'}), ''); +});