From fc9890235d1deb409e4af914570b4f1fa2b662ad Mon Sep 17 00:00:00 2001 From: landmineHQ Date: Sat, 26 Sep 2026 02:52:21 -0400 Subject: [PATCH] fix(ui): decode room id from URL and encode it when writing back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The room id was read from the query string with a hand-rolled parser that never decoded it, while the id was written back into the URL without any encoding: getFromURL = (key, search) => search.slice(1).split('&') .find((param) => param.startsWith(`${key}=`))?.split('=')[1]; window.history.pushState({roomId: id}, '', id ? `?room=${id}` : '?'); Browsers percent-encode non-ASCII characters when a URL is serialized, so a room named "不辞" is shared as `?room=%E4%B8%8D%E8%BE%9E`, but the client then joins the literal `%E4%B8%8D%E8%BE%9E` room, which does not exist: -> {"type":"join","payload":{"id":"%E4%B8%8D%E8%BE%9E"}} <- close 1000 "room with id %E4%B8%8D%E8%BE%9E does not exist" The same asymmetry also broke ids containing `&`, `=`, `+`, `#` or `%`, and made ids that already contain percent escapes gain another encoding level on every write, producing broken (double encoded) share links. Use the platform URL API, which decodes on read and encodes on write exactly once: new URLSearchParams(search).get(key) ?? undefined const params = new URLSearchParams(); params.set('room', id); window.history.pushState({roomId: id}, '', `?${params.toString()}`); No server change is needed: the room id stays the raw, human-readable string that the ws `create`/`join` payload and the server-side room map already use, and existing share links keep working. Verified: URL round-trip for 不辞 / "my room" / "a+b" / "a&b=c" / "100%", protocol-level e2e against a live server (encoded id -> "does not exist", decoded id -> joins, cleanup on owner leave), `yarn build` (tsc + vite) and `prettier --check src/useRoomID.ts`. --- ui/src/useRoomID.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/src/useRoomID.ts b/ui/src/useRoomID.ts index d2acec02..dfaf3b81 100644 --- a/ui/src/useRoomID.ts +++ b/ui/src/useRoomID.ts @@ -5,12 +5,7 @@ export const getRoomFromURL = (): string | undefined => getFromURL('room'); export const getFromURL = ( key: string, search: string = window.location.search -): string | undefined => - search - .slice(1) - .split('&') - .find((param) => param.startsWith(`${key}=`)) - ?.split('=')[1]; +): string | undefined => new URLSearchParams(search).get(key) ?? undefined; export const useRoomID = (): [string | undefined, (v?: string) => void] => { const [state, setState] = React.useState(() => getRoomFromURL()); @@ -25,7 +20,12 @@ export const useRoomID = (): [string | undefined, (v?: string) => void] => { (id) => setState((oldId?: string) => { if (oldId !== id) { - window.history.pushState({roomId: id}, '', id ? `?room=${id}` : '?'); + const params = new URLSearchParams(); + if (id) { + params.set('room', id); + } + const query = params.toString(); + window.history.pushState({roomId: id}, '', query ? `?${query}` : '?'); } return id; }),