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 = ''; }