From ee99c3d02d8146c667e0fd7e06bd755f52d02f82 Mon Sep 17 00:00:00 2001 From: kimjeongwonn Date: Fri, 15 Aug 2025 16:06:38 +0900 Subject: [PATCH 1/2] fix: make encodeQueryValue RFC 3986 compliant --- packages/nuqs/src/lib/url-encoding.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/nuqs/src/lib/url-encoding.ts b/packages/nuqs/src/lib/url-encoding.ts index 00d53ef3d..9fdb46022 100644 --- a/packages/nuqs/src/lib/url-encoding.ts +++ b/packages/nuqs/src/lib/url-encoding.ts @@ -42,6 +42,13 @@ export function encodeQueryValue(input: string): string { .replace(/`/g, '%60') .replace(//g, '%3E') + .replace(/{/g, '%7B') + .replace(/}/g, '%7D') + .replace(/\|/g, '%7C') + .replace(/\\/g, '%5C') + .replace(/\^/g, '%5E') + .replace(/`/g, '%60') + .replace(/\?/g, '%3F') // Encode invisible ASCII control characters .replace(/[\x00-\x1F]/g, char => encodeURIComponent(char)) ) From ae6fd15ef0af03034b4192ab6a55ad4e8530568f Mon Sep 17 00:00:00 2001 From: kimjeongwonn Date: Fri, 15 Aug 2025 16:07:49 +0900 Subject: [PATCH 2/2] test: update specs to match RFC 3986-compliant encodeQueryValue --- packages/nuqs/src/lib/url-encoding.test.ts | 10 +++++++--- packages/nuqs/src/serializer.test.ts | 10 ++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/nuqs/src/lib/url-encoding.test.ts b/packages/nuqs/src/lib/url-encoding.test.ts index f98cf94be..2e2ea29c7 100644 --- a/packages/nuqs/src/lib/url-encoding.test.ts +++ b/packages/nuqs/src/lib/url-encoding.test.ts @@ -36,7 +36,7 @@ describe('url-encoding/encodeQueryValue', () => { expect(encodeQueryValue(input)).toBe(input) }) test('Other special characters are passed through', () => { - const input = '-._~!$()*,;=:@/?[]{}\\|^' + const input = '-._~!$()*,;=:@/[]' expect(encodeQueryValue(input)).toBe(input) }) test('practical use-cases', () => { @@ -80,7 +80,9 @@ describe('url-encoding/renderQueryString', () => { test('encoding', () => { const search = new URLSearchParams() search.set('test', '-._~!$()*,;=:@/?[]{}\\|^') - expect(renderQueryString(search)).toBe('?test=-._~!$()*,;=:@/?[]{}\\|^') + expect(renderQueryString(search)).toBe( + '?test=-._~!$()*,;=:@/%3F[]%7B%7D%5C%7C%5E' + ) }) test('decoding', () => { const search = new URLSearchParams() @@ -132,7 +134,9 @@ describe('url-encoding/renderQueryString', () => { const search = new URLSearchParams() search.set('filter', value) const query = renderQueryString(search) - expect(query.slice('?filter='.length)).toBe(value) + expect(query.slice('?filter='.length)).toBe( + 'leftOfBicycleLane:car_lanes,curb%7CpavementHasShops:true%7CpavementWidth:narrow' + ) } { const url = new URL( diff --git a/packages/nuqs/src/serializer.test.ts b/packages/nuqs/src/serializer.test.ts index 0f6573b95..ac6857bfd 100644 --- a/packages/nuqs/src/serializer.test.ts +++ b/packages/nuqs/src/serializer.test.ts @@ -136,7 +136,7 @@ describe('serializer', () => { json: { foo: 'bar' } }) expect(result).toBe( - '?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}' + '?int=0&str=&bool=false&arr=&json=%7B%22foo%22:%22bar%22%7D' ) }) it('supports a global clearOnDefault option', () => { @@ -158,7 +158,7 @@ describe('serializer', () => { json: { foo: 'bar' } }) expect(result).toBe( - '?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}' + '?int=0&str=&bool=false&arr=&json=%7B%22foo%22:%22bar%22%7D' ) }) it('gives precedence to parser clearOnDefault over global clearOnDefault', () => { @@ -191,7 +191,7 @@ describe('serializer', () => { it('supports ? in the values', () => { const serialize = createSerializer(parsers) const result = serialize({ str: 'foo?bar', int: 1, bool: true }) - expect(result).toBe('?str=foo?bar&int=1&bool=true') + expect(result).toBe('?str=foo%3Fbar&int=1&bool=true') }) it('supports & in the base', () => { // Repro for https://github.com/47ng/nuqs/issues/812 @@ -199,6 +199,8 @@ describe('serializer', () => { const result = serialize('https://example.com/path?issue=is?here', { str: 'foo?bar' }) - expect(result).toBe('https://example.com/path?issue=is?here&str=foo?bar') + expect(result).toBe( + 'https://example.com/path?issue=is%3Fhere&str=foo%3Fbar' + ) }) })