From 0f7715b183331e4d2972c8302eabac444be2e20c Mon Sep 17 00:00:00 2001 From: Nicholas Date: Mon, 21 Sep 2026 22:04:26 -0700 Subject: [PATCH] fix(shared): do not throw on a malformed URL fragment in the dev browser handshake extractDevBrowserFromURL runs during a development instance's handshake and passed the URL fragment straight to decodeURI. A fragment that is not valid percent-encoding, such as a truncated `#%E0%A4%A`, threw `URIError: URI malformed`, which stopped initialization before the client was loaded. Decode the fragment only when it decodes, and otherwise work on it as it is. The legacy dev browser marker is still removed from such a fragment. Fixes #9667 --- .changeset/devbrowser-malformed-url-fragment.md | 6 ++++++ packages/shared/src/__tests__/devbrowser.spec.ts | 16 ++++++++++++++++ packages/shared/src/devBrowser.ts | 10 +++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/devbrowser-malformed-url-fragment.md diff --git a/.changeset/devbrowser-malformed-url-fragment.md b/.changeset/devbrowser-malformed-url-fragment.md new file mode 100644 index 00000000000..681874757e3 --- /dev/null +++ b/.changeset/devbrowser-malformed-url-fragment.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': patch +'@clerk/clerk-js': patch +--- + +Fix Clerk failing to load in a development instance when the page URL has a fragment that is not valid percent-encoding, such as a truncated `#%E0%A4%A` from a clipped or wrapped link. Initialization threw `URIError: URI malformed` during the handshake, so the session was never loaded and every page showed the sign-in screen until the fragment was removed. Clerk now leaves such a fragment as it is and loads normally. diff --git a/packages/shared/src/__tests__/devbrowser.spec.ts b/packages/shared/src/__tests__/devbrowser.spec.ts index 4501c5aa78f..c3a973d187e 100644 --- a/packages/shared/src/__tests__/devbrowser.spec.ts +++ b/packages/shared/src/__tests__/devbrowser.spec.ts @@ -61,6 +61,21 @@ describe('extractDevBrowserFromURL(url)', () => { expect(replaceStateMock).not.toHaveBeenCalled(); }); + it('does not throw when the url hash is not valid percent-encoding', () => { + expect(extractDevBrowserFromURL(new URL('/foo?__clerk_db_jwt=token#%E0%A4%A', DUMMY_URL_BASE))).toEqual('token'); + expect(replaceStateMock.mock.calls[0][2].href).toEqual(new URL('/foo#%E0%A4%A', DUMMY_URL_BASE).href); + }); + + it('does not call replaceState for a malformed hash with nothing to remove', () => { + expect(extractDevBrowserFromURL(new URL('/foo#%E0%A4%A', DUMMY_URL_BASE))).toEqual(''); + expect(replaceStateMock).not.toHaveBeenCalled(); + }); + + it('removes the legacy dev browser from a hash that is not valid percent-encoding', () => { + expect(extractDevBrowserFromURL(new URL('/foo#%E0%A4%A__clerk_db_jwt[token2]', DUMMY_URL_BASE))).toEqual(''); + expect(replaceStateMock.mock.calls[0][2].href).toEqual(new URL('/foo#%E0%A4%A', DUMMY_URL_BASE).href); + }); + const testCases: Array<[string, string]> = [ ['', ''], ['foo', ''], @@ -69,6 +84,7 @@ describe('extractDevBrowserFromURL(url)', () => { ['/foo?__clerk_db_jwt=token', 'token'], ['?__clerk_db_jwt=token#foo', 'token'], ['/foo?bar=42&__clerk_db_jwt=token#qux__clerk_db_jwt[token2]', 'token'], + ['/foo?__clerk_db_jwt=token#%E0%A4%A', 'token'], ]; test.each(testCases)( diff --git a/packages/shared/src/devBrowser.ts b/packages/shared/src/devBrowser.ts index f8157217304..788d3500556 100644 --- a/packages/shared/src/devBrowser.ts +++ b/packages/shared/src/devBrowser.ts @@ -46,6 +46,14 @@ const removeDevBrowserFromURLSearchParams = (_url: URL) => { return url; }; +const decodeURISafely = (value: string) => { + try { + return decodeURI(value); + } catch { + return value; + } +}; + /** * Removes the __clerk_db_jwt dev browser from the URL hash, as well as * the legacy __dev_session from the URL searchParams @@ -62,7 +70,7 @@ const removeLegacyDevBrowser = (_url: URL) => { const DEV_BROWSER_LEGACY_KEY = '__dev_session'; const url = new URL(_url); url.searchParams.delete(DEV_BROWSER_LEGACY_KEY); - url.hash = decodeURI(url.hash).replace(DEV_BROWSER_MARKER_REGEXP, ''); + url.hash = decodeURISafely(url.hash).replace(DEV_BROWSER_MARKER_REGEXP, ''); if (url.href.endsWith('#')) { url.hash = ''; }