Skip to content

Commit 92eb703

Browse files
committed
Fix path-like query strings losing structure when sorting parameters
Fixes #193
1 parent 0d866eb commit 92eb703

File tree

2 files changed

+41
-0
lines changed

2 files changed

+41
-0
lines changed

index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,21 @@ export default function normalizeUrl(urlString, options) {
240240

241241
// Sort query parameters
242242
if (options.sortQueryParameters) {
243+
const originalSearch = urlObject.search;
243244
urlObject.searchParams.sort();
244245

245246
// Calling `.sort()` encodes the search parameters, so we need to decode them again.
246247
try {
247248
urlObject.search = decodeURIComponent(urlObject.search);
248249
} catch {}
250+
251+
// Fix parameters that originally had no equals sign but got one added by URLSearchParams
252+
const partsWithoutEquals = originalSearch.slice(1).split('&').filter(p => p && !p.includes('='));
253+
for (const part of partsWithoutEquals) {
254+
const decoded = decodeURIComponent(part);
255+
// Only replace at word boundaries to avoid partial matches
256+
urlObject.search = urlObject.search.replace(`?${decoded}=`, `?${decoded}`).replace(`&${decoded}=`, `&${decoded}`);
257+
}
249258
}
250259

251260
if (options.removeTrailingSlash) {

test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,35 @@ test('encoded backslashes do not get decoded', t => {
425425
// Non-encoded backslashes should remain as-is.
426426
t.is(normalizeUrl('https://foo.com/something\\else/great'), 'https://foo.com/something/else/great');
427427
});
428+
429+
test('path-like query strings without equals signs are preserved', t => {
430+
// Issue #193 - Path-like query strings should not get '=' appended
431+
t.is(normalizeUrl('https://example.com/index.php?/Some/Route/To/Path/12345'), 'https://example.com/index.php?/Some/Route/To/Path/12345');
432+
t.is(normalizeUrl('https://example.com/script.php?/api/v1/users/123'), 'https://example.com/script.php?/api/v1/users/123');
433+
t.is(normalizeUrl('https://example.com/app.php?/admin/dashboard'), 'https://example.com/app.php?/admin/dashboard');
434+
// Note: trailing slash is removed by default removeTrailingSlash option
435+
t.is(normalizeUrl('https://example.com/index.php?/path/'), 'https://example.com/index.php?/path');
436+
// With removeTrailingSlash disabled, trailing slash is preserved
437+
t.is(normalizeUrl('https://example.com/index.php?/path/', {removeTrailingSlash: false}), 'https://example.com/index.php?/path/');
438+
439+
// Mixed parameters: path-like without '=' and regular with '='
440+
t.is(normalizeUrl('https://example.com/index.php?b=2&/path/to/resource&a=1'), 'https://example.com/index.php?/path/to/resource&a=1&b=2');
441+
t.is(normalizeUrl('https://example.com/index.php?/path&param=value'), 'https://example.com/index.php?/path&param=value');
442+
443+
// Regular parameters with empty values should keep '='
444+
t.is(normalizeUrl('https://example.com/index.php?key='), 'https://example.com/index.php?key=');
445+
t.is(normalizeUrl('https://example.com/index.php?key=&another='), 'https://example.com/index.php?another=&key=');
446+
447+
// Parameters without values should not get '=' added
448+
t.is(normalizeUrl('https://example.com/index.php?key'), 'https://example.com/index.php?key');
449+
t.is(normalizeUrl('https://example.com/index.php?a&b&c'), 'https://example.com/index.php?a&b&c');
450+
451+
// With sortQueryParameters disabled, original format is preserved
452+
t.is(normalizeUrl('https://example.com/index.php?/Some/Route/To/Path/12345', {sortQueryParameters: false}), 'https://example.com/index.php?/Some/Route/To/Path/12345');
453+
t.is(normalizeUrl('https://example.com/index.php?key', {sortQueryParameters: false}), 'https://example.com/index.php?key');
454+
455+
// Safety: parameters with similar names should not interfere with each other
456+
t.is(normalizeUrl('https://example.com/index.php?/path&/longpath'), 'https://example.com/index.php?/longpath&/path');
457+
t.is(normalizeUrl('https://example.com/index.php?key&anotherkey'), 'https://example.com/index.php?anotherkey&key');
458+
t.is(normalizeUrl('https://example.com/index.php?/api&/api/v1/users'), 'https://example.com/index.php?/api&/api/v1/users');
459+
});

0 commit comments

Comments
 (0)