URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

Description

@davidbarratt

Preliminary Checks

  • I have reviewed the documentation: https://clerk.com/docs
  • I have searched for existing issues: https://github.com/clerk/javascript/issues
  • I have not already reached out to Clerk support via email or Discord
  • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

Reproduction

https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

Publishable key

Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

Description

ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

Two properties widen the blast radius well past the one cookie that is malformed:

  • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
  • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

Steps to reproduce:

  1. npm install @clerk/backend@3.15.1
  2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
    import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
  3. Observe URIError: URI malformed.

Expected behavior:

A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

Actual behavior:

URIError: URI malformed
at decodeURIComponent (<anonymous>)
at String.replace (<anonymous>)
at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)

Inputs that throw, all verified against 3.15.1:

Cookie headerWhy
x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
x=%98lone continuation byte
x=%C0%80overlong UTF-8 encoding

The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

Error running the exported Web Handler: URIError: URI malformed
at decodeURIComponent (<anonymous>)
at String.replace (<anonymous>)
at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)

Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

Percent-encoding is case-insensitive, so both should decode to the same value.

Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

Environment

 System:
OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
CPU: (5) arm64
Binaries:
Node: 24.18.0
npm: 11.16.0
pnpm: 11.11.0
npmPackages:
@clerk/backend: 3.15.1 => 3.15.1
Also reproduced in production on:
Vercel, Node.js 22.x runtime
next: 16.2.6
@clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

      Description

      @davidbarratt

      Preliminary Checks

      • I have reviewed the documentation: https://clerk.com/docs
      • I have searched for existing issues: https://github.com/clerk/javascript/issues
      • I have not already reached out to Clerk support via email or Discord
      • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

      Reproduction

      https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

      Publishable key

      Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

      Description

      ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

      privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

      decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

      Two properties widen the blast radius well past the one cookie that is malformed:

      • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
      • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

      Steps to reproduce:

      1. npm install @clerk/backend@3.15.1
      2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
        import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
      3. Observe URIError: URI malformed.

      Expected behavior:

      A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

      Actual behavior:

      URIError: URI malformed
      at decodeURIComponent (<anonymous>)
      at String.replace (<anonymous>)
      at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
      at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
      at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
      at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
      

      Inputs that throw, all verified against 3.15.1:

      Cookie headerWhy
      x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
      x=%98lone continuation byte
      x=%C0%80overlong UTF-8 encoding

      The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

      How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

      Error running the exported Web Handler: URIError: URI malformed
      at decodeURIComponent (<anonymous>)
      at String.replace (<anonymous>)
      at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
      at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
      at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
      at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
      

      Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

      Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

      Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

      decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

      Percent-encoding is case-insensitive, so both should decode to the same value.

      Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

      Environment

       System:
      OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
      CPU: (5) arm64
      Binaries:
      Node: 24.18.0
      npm: 11.16.0
      pnpm: 11.11.0
      npmPackages:
      @clerk/backend: 3.15.1 => 3.15.1
      Also reproduced in production on:
      Vercel, Node.js 22.x runtime
      next: 16.2.6
      @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

          Description

          @davidbarratt

          Preliminary Checks

          • I have reviewed the documentation: https://clerk.com/docs
          • I have searched for existing issues: https://github.com/clerk/javascript/issues
          • I have not already reached out to Clerk support via email or Discord
          • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

          Reproduction

          https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

          Publishable key

          Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

          Description

          ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

          privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

          decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

          Two properties widen the blast radius well past the one cookie that is malformed:

          • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
          • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

          Steps to reproduce:

          1. npm install @clerk/backend@3.15.1
          2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
            import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
          3. Observe URIError: URI malformed.

          Expected behavior:

          A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

          Actual behavior:

          URIError: URI malformed
          at decodeURIComponent (<anonymous>)
          at String.replace (<anonymous>)
          at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
          at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
          at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
          at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
          

          Inputs that throw, all verified against 3.15.1:

          Cookie headerWhy
          x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
          x=%98lone continuation byte
          x=%C0%80overlong UTF-8 encoding

          The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

          How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

          Error running the exported Web Handler: URIError: URI malformed
          at decodeURIComponent (<anonymous>)
          at String.replace (<anonymous>)
          at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
          at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
          at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
          at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
          

          Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

          Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

          Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

          decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

          Percent-encoding is case-insensitive, so both should decode to the same value.

          Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

          Environment

           System:
          OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
          CPU: (5) arm64
          Binaries:
          Node: 24.18.0
          npm: 11.16.0
          pnpm: 11.11.0
          npmPackages:
          @clerk/backend: 3.15.1 => 3.15.1
          Also reproduced in production on:
          Vercel, Node.js 22.x runtime
          next: 16.2.6
          @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

              Description

              @davidbarratt

              Preliminary Checks

              • I have reviewed the documentation: https://clerk.com/docs
              • I have searched for existing issues: https://github.com/clerk/javascript/issues
              • I have not already reached out to Clerk support via email or Discord
              • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

              Reproduction

              https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

              Publishable key

              Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

              Description

              ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

              privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

              decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

              Two properties widen the blast radius well past the one cookie that is malformed:

              • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
              • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

              Steps to reproduce:

              1. npm install @clerk/backend@3.15.1
              2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
                import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
              3. Observe URIError: URI malformed.

              Expected behavior:

              A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

              Actual behavior:

              URIError: URI malformed
              at decodeURIComponent (<anonymous>)
              at String.replace (<anonymous>)
              at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
              at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
              at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
              at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
              

              Inputs that throw, all verified against 3.15.1:

              Cookie headerWhy
              x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
              x=%98lone continuation byte
              x=%C0%80overlong UTF-8 encoding

              The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

              How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

              Error running the exported Web Handler: URIError: URI malformed
              at decodeURIComponent (<anonymous>)
              at String.replace (<anonymous>)
              at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
              at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
              at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
              at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
              

              Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

              Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

              Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

              decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

              Percent-encoding is case-insensitive, so both should decode to the same value.

              Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

              Environment

               System:
              OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
              CPU: (5) arm64
              Binaries:
              Node: 24.18.0
              npm: 11.16.0
              pnpm: 11.11.0
              npmPackages:
              @clerk/backend: 3.15.1 => 3.15.1
              Also reproduced in production on:
              Vercel, Node.js 22.x runtime
              next: 16.2.6
              @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

                  Description

                  @davidbarratt

                  Preliminary Checks

                  • I have reviewed the documentation: https://clerk.com/docs
                  • I have searched for existing issues: https://github.com/clerk/javascript/issues
                  • I have not already reached out to Clerk support via email or Discord
                  • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

                  Reproduction

                  https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

                  Publishable key

                  Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

                  Description

                  ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

                  privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

                  decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

                  Two properties widen the blast radius well past the one cookie that is malformed:

                  • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
                  • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

                  Steps to reproduce:

                  1. npm install @clerk/backend@3.15.1
                  2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
                    import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
                  3. Observe URIError: URI malformed.

                  Expected behavior:

                  A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

                  Actual behavior:

                  URIError: URI malformed
                  at decodeURIComponent (<anonymous>)
                  at String.replace (<anonymous>)
                  at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
                  at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
                  at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
                  at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
                  

                  Inputs that throw, all verified against 3.15.1:

                  Cookie headerWhy
                  x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
                  x=%98lone continuation byte
                  x=%C0%80overlong UTF-8 encoding

                  The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

                  How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

                  Error running the exported Web Handler: URIError: URI malformed
                  at decodeURIComponent (<anonymous>)
                  at String.replace (<anonymous>)
                  at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
                  at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
                  at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
                  at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
                  

                  Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

                  Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

                  Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

                  decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

                  Percent-encoding is case-insensitive, so both should decode to the same value.

                  Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

                  Environment

                   System:
                  OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
                  CPU: (5) arm64
                  Binaries:
                  Node: 24.18.0
                  npm: 11.16.0
                  pnpm: 11.11.0
                  npmPackages:
                  @clerk/backend: 3.15.1 => 3.15.1
                  Also reproduced in production on:
                  Vercel, Node.js 22.x runtime
                  next: 16.2.6
                  @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

                      Description

                      @davidbarratt

                      Preliminary Checks

                      • I have reviewed the documentation: https://clerk.com/docs
                      • I have searched for existing issues: https://github.com/clerk/javascript/issues
                      • I have not already reached out to Clerk support via email or Discord
                      • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

                      Reproduction

                      https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

                      Publishable key

                      Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

                      Description

                      ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

                      privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

                      decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

                      Two properties widen the blast radius well past the one cookie that is malformed:

                      • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
                      • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

                      Steps to reproduce:

                      1. npm install @clerk/backend@3.15.1
                      2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
                        import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
                      3. Observe URIError: URI malformed.

                      Expected behavior:

                      A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

                      Actual behavior:

                      URIError: URI malformed
                      at decodeURIComponent (<anonymous>)
                      at String.replace (<anonymous>)
                      at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
                      at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
                      at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
                      at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
                      

                      Inputs that throw, all verified against 3.15.1:

                      Cookie headerWhy
                      x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
                      x=%98lone continuation byte
                      x=%C0%80overlong UTF-8 encoding

                      The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

                      How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

                      Error running the exported Web Handler: URIError: URI malformed
                      at decodeURIComponent (<anonymous>)
                      at String.replace (<anonymous>)
                      at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
                      at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
                      at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
                      at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
                      

                      Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

                      Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

                      Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

                      decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

                      Percent-encoding is case-insensitive, so both should decode to the same value.

                      Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

                      Environment

                       System:
                      OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
                      CPU: (5) arm64
                      Binaries:
                      Node: 24.18.0
                      npm: 11.16.0
                      pnpm: 11.11.0
                      npmPackages:
                      @clerk/backend: 3.15.1 => 3.15.1
                      Also reproduced in production on:
                      Vercel, Node.js 22.x runtime
                      next: 16.2.6
                      @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

                          Description

                          @davidbarratt

                          Preliminary Checks

                          • I have reviewed the documentation: https://clerk.com/docs
                          • I have searched for existing issues: https://github.com/clerk/javascript/issues
                          • I have not already reached out to Clerk support via email or Discord
                          • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

                          Reproduction

                          https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

                          Publishable key

                          Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

                          Description

                          ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

                          privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

                          decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

                          Two properties widen the blast radius well past the one cookie that is malformed:

                          • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
                          • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

                          Steps to reproduce:

                          1. npm install @clerk/backend@3.15.1
                          2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
                            import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
                          3. Observe URIError: URI malformed.

                          Expected behavior:

                          A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

                          Actual behavior:

                          URIError: URI malformed
                          at decodeURIComponent (<anonymous>)
                          at String.replace (<anonymous>)
                          at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
                          at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
                          at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
                          at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
                          

                          Inputs that throw, all verified against 3.15.1:

                          Cookie headerWhy
                          x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
                          x=%98lone continuation byte
                          x=%C0%80overlong UTF-8 encoding

                          The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

                          How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

                          Error running the exported Web Handler: URIError: URI malformed
                          at decodeURIComponent (<anonymous>)
                          at String.replace (<anonymous>)
                          at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
                          at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
                          at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
                          at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
                          

                          Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

                          Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

                          Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

                          decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

                          Percent-encoding is case-insensitive, so both should decode to the same value.

                          Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

                          Environment

                           System:
                          OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
                          CPU: (5) arm64
                          Binaries:
                          Node: 24.18.0
                          npm: 11.16.0
                          pnpm: 11.11.0
                          npmPackages:
                          @clerk/backend: 3.15.1 => 3.15.1
                          Also reproduced in production on:
                          Vercel, Node.js 22.x runtime
                          next: 16.2.6
                          @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              URIError: URI malformed — ClerkRequest.parseCookies throws on any malformed percent-escape in the Cookie header #9333

                              Description

                              @davidbarratt

                              Preliminary Checks

                              • I have reviewed the documentation: https://clerk.com/docs
                              • I have searched for existing issues: https://github.com/clerk/javascript/issues
                              • I have not already reached out to Clerk support via email or Discord
                              • This issue is not a question, general help request, or anything other than a bug report directly related to Clerk

                              Reproduction

                              https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030

                              Publishable key

                              Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.

                              Description

                              ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:

                              privateparseCookies(req: Request){constcookiesRecord=parse(this.decodeCookieValue(req.headers.get('cookie')||''));returnnewMap(Object.entries(cookiesRecord));}privatedecodeCookieValue(str: string){returnstr ? str.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent) : str;}

                              decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.

                              Two properties widen the blast radius well past the one cookie that is malformed:

                              • The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
                              • The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.

                              Steps to reproduce:

                              1. npm install @clerk/backend@3.15.1
                              2. Call createClerkRequest with a request whose Cookie header contains a malformed escape:
                                import{createClerkRequest}from'@clerk/backend/internal';createClerkRequest(newRequest('https://example.com/',{headers: {cookie: '__session=abc; analytics_id=%E2%9'},}));
                              3. Observe URIError: URI malformed.

                              Expected behavior:

                              A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.

                              Actual behavior:

                              URIError: URI malformed
                              at decodeURIComponent (<anonymous>)
                              at String.replace (<anonymous>)
                              at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
                              at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
                              at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
                              at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
                              

                              Inputs that throw, all verified against 3.15.1:

                              Cookie headerWhy
                              x=%E2%9escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte
                              x=%98lone continuation byte
                              x=%C0%80overlong UTF-8 encoding

                              The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.

                              How it surfaces in a Next.js app.@clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:

                              Error running the exported Web Handler: URIError: URI malformed
                              at decodeURIComponent (<anonymous>)
                              at String.replace (<anonymous>)
                              at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
                              at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
                              at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
                              at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
                              

                              Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.

                              Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.

                              Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:

                              decodeCookieValue('x=%c3%a9')// 'x=%c3%a9' — left rawdecodeCookieValue('x=%C3%A9')// 'x=é'

                              Percent-encoding is case-insensitive, so both should decode to the same value.

                              Prior public report.ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.

                              Environment

                               System:
                              OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
                              CPU: (5) arm64
                              Binaries:
                              Node: 24.18.0
                              npm: 11.16.0
                              pnpm: 11.11.0
                              npmPackages:
                              @clerk/backend: 3.15.1 => 3.15.1
                              Also reproduced in production on:
                              Vercel, Node.js 22.x runtime
                              next: 16.2.6
                              @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions