Skip to content

Commit 905edc9

Browse files
authored
fix: Encode special characters in keys (#600)
* fix: Encode special characters in keys Closes #599. * test: Start with less encoding to verify first parsing
1 parent de50388 commit 905edc9

File tree

4 files changed

+61
-1
lines changed

4 files changed

+61
-1
lines changed
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference types="cypress" />
2+
3+
it('Reproduction for issue #599', () => {
4+
// Start without encoding for most characters
5+
cy.visit(
6+
'/app/repro-599?a %26b%3Fc%3Dd%23e%f%2Bg"h\'i`j<k>l(m)n*o,p.q:r;s/t=init'
7+
)
8+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
9+
cy.get('input').should('have.value', 'init')
10+
cy.get('p').should('have.text', 'init')
11+
cy.get('button').click()
12+
cy.get('input').should('have.value', 'works')
13+
cy.get('p').should('have.text', 'works')
14+
cy.location('search').should(
15+
'eq',
16+
'?a%20%26b%3Fc%3Dd%23e%f%2Bg%22h%27i`j%3Ck%3El(m)n*o,p.q:r;s/t=works'
17+
)
18+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client'
2+
3+
import { parseAsString, useQueryState, useQueryStates } from 'nuqs'
4+
import { Suspense } from 'react'
5+
6+
export default function Page() {
7+
return (
8+
<Suspense>
9+
<Client />
10+
</Suspense>
11+
)
12+
}
13+
14+
const key = 'a &b?c=d#e%f+g"h\'i`j<k>l(m)n*o,p.q:r;s/t'
15+
const parser = parseAsString.withDefault('')
16+
17+
function Client() {
18+
const [a, setValue] = useQueryState(key, parser)
19+
const [{ [key]: b }] = useQueryStates({ [key]: parser })
20+
return (
21+
<>
22+
<input value={a} onChange={e => setValue(e.target.value)} />
23+
<p>{b}</p>
24+
<button onClick={() => setValue('works')}>Test</button>
25+
</>
26+
)
27+
}

packages/nuqs/src/url-encoding.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ describe('url-encoding/renderQueryString', () => {
117117
expect(search).toBe(url.search)
118118
}
119119
})
120+
test('keys with special characters get escaped', () => {
121+
const search = new URLSearchParams()
122+
search.set('a &b?c=d#e%f+g"h\'i`j<k>l(m)n*o,p.q:r;s/t', 'value')
123+
expect(renderQueryString(search)).toBe(
124+
'?a %26b%3Fc%3Dd%23e%f%2Bg"h\'i`j<k>l(m)n*o,p.q:r;s/t=value'
125+
)
126+
})
120127
})
121128

122129
test.skip('encodeURI vs encodeURIComponent vs custom encoding', () => {

packages/nuqs/src/url-encoding.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ export function renderQueryString(search: URLSearchParams) {
44
}
55
const query: string[] = []
66
for (const [key, value] of search.entries()) {
7-
query.push(`${key}=${encodeQueryValue(value)}`)
7+
// Replace disallowed characters in keys,
8+
// see https://github.com/47ng/nuqs/issues/599
9+
const safeKey = key
10+
.replace(/#/g, '%23')
11+
.replace(/&/g, '%26')
12+
.replace(/\+/g, '%2B')
13+
.replace(/=/g, '%3D')
14+
.replace(/\?/g, '%3F')
15+
query.push(`${safeKey}=${encodeQueryValue(value)}`)
816
}
917
return '?' + query.join('&')
1018
}

0 commit comments

Comments
 (0)