Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading
, '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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading
, '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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading
, '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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading
, '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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading
, '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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading
, '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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading
, '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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/init-blacksmith/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/init/action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ inputs:
node-version:
description: 'The node version to use'
required: false
default: '24.15.0'
default: '22'
playwright-enabled:
description: 'Enable Playwright?'
required: false
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,10 +218,7 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: 24.15.0
test-filter: "**"
filter-label: "**"
- node-version: 20.19.0
- node-version: 22
test-filter: "**"
filter-label: "**"

Expand DownExpand Up@@ -260,7 +257,7 @@ jobs:
- name: Run Typedoc tests
run: |
# Only run Typedoc tests for one matrix version and main test run
if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then
pnpm turbo run //#test:typedoc
fi
env:
Expand DownExpand Up@@ -504,7 +501,7 @@ jobs:
uses: ./.github/actions/init-blacksmith
with:
turbo-enabled: true
node-version: 24.15.0
node-version: 22
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-summarize: ${{ env.TURBO_SUMMARIZE }}
turbo-team: ${{ vars.TURBO_TEAM }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -524,7 +524,7 @@ jobs:

strategy:
matrix:
version: [24] # NOTE: 20 is cached in the main release workflow
version: [22] # NOTE: 18 is cached in the main release workflow

steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
24.15.0
22.11.0
2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,7 +152,7 @@
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": {
"node": ">=24.15.0",
"node": ">=22.11.0",
"pnpm": ">=10.33.0"
},
"pnpm": {
Expand Down
8 changes: 2 additions & 6 deletions packages/backend/src/__tests__/proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,11 +572,7 @@ describe('proxy', () => {
expect(response.status).toBe(200);
});

it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => {
// Node 24's bundled undici tightened the instanceof AbortSignal check on
// RequestInit.signal, which throws on cross-realm signals carried by
// framework Request subclasses. Until we bridge abort propagation via an
// in-realm AbortController, the signal is intentionally omitted.
it('propagates abort signal to upstream fetch', async () => {
const mockResponse = new Response(JSON.stringify({}), { status: 200 });
mockFetch.mockResolvedValue(mockResponse);

Expand All@@ -591,7 +587,7 @@ describe('proxy', () => {
});

const [, options] = mockFetch.mock.calls[0];
expect(options.signal).toBeUndefined();
expect(options.signal).toBe(request.signal);
});

it('includes Cache-Control: no-store on error responses', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/backend/src/proxy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend

try {
// Make the proxied request
// TODO: Restore abort cascade via an in-realm AbortController bridge,
// and consider adding AbortSignal.timeout(30_000) via AbortSignal.any().
// `request.signal` is intentionally omitted: Node 24's bundled undici
// tightened the instanceof AbortSignal check on RequestInit.signal, which
// rejects cross-realm signals carried by framework Request subclasses.
// TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'manual',
signal: request.signal,
};

// Only set duplex when body is present (required for streaming bodies)
Expand Down
20 changes: 1 addition & 19 deletions packages/backend/src/tokens/clerkRequest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,7 @@ class ClerkRequest extends Request {
// https://github.com/nodejs/undici/issues/2155
// https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854
const url = typeof input !== 'string' && 'url' in input ? input.url : String(input);
// When cloning a Request by passing it as init, hide its `signal`. Undici's
// Request constructor in Node 24 performs a strict instanceof check on the
// signal and rejects ones from a different realm (e.g. NextRequest). Using a
// Proxy keeps property access lazy so environments that don't implement
// optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work.
let cloneInit: RequestInit | undefined;
if (init) {
cloneInit = init;
} else if (typeof input !== 'string') {
cloneInit = new Proxy(input as Request, {
get(target, prop) {
if (prop === 'signal') {
return undefined;
}
return Reflect.get(target, prop, target);
},
}) as unknown as RequestInit;
}
super(url, cloneInit);
super(url, init || typeof input === 'string' ? undefined : input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the context
head -40 packages/backend/src/tokens/clerkRequest.ts | cat -n

Repository: clerk/javascript

Length of output: 2270


🏁 Script executed:

# Verify JavaScript operator precedence using Node.js
node <<'EOF'// Test the precedence claim// The expression: init || typeof input === 'string' ? undefined : input// Simulating different valuesconst test = (init, input) => { // Original expression (as reviewer claims it parses) const original = (init || typeof input === 'string') ? undefined : input; // Proposed fix const fixed = init || (typeof input === 'string' ? undefined : input); console.log(`init="${init}", input="${input}"`); console.log(` Original (claimed): ${original}`); console.log(` Fixed: ${fixed}`); console.log(` Different? ${original !== fixed}`); console.log('');};// Test casestest(null, 'string'); // init falsy, input is stringtest(null, {}); // init falsy, input is objecttest({data: 'x'}, 'string'); // init truthy, input is stringtest({}, {}); // both truthyEOF

Repository: clerk/javascript

Length of output: 518


Fix operator precedence bug on Line 29 – init parameter is being dropped.

Due to JavaScript operator precedence, init || typeof input === 'string' ? undefined : input parses as (init || typeof input === 'string') ? undefined : input, causing any provided init to be ignored when it's truthy. This breaks request initialization/cloning when init is passed.

Suggested fix
- super(url, init || typeof input === 'string' ? undefined : input);+ super(url, init || (typeof input === 'string' ? undefined : input));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/src/tokens/clerkRequest.ts` at line 29, The super call in
the constructor of ClerkRequest uses the expression init || typeof input ===
'string' ? undefined : input which mis-parses due to operator precedence and
drops a truthy init; change the second argument to use init as the primary value
and only fall back to the ternary when init is falsy by parenthesizing the
ternary (i.e., pass init || (typeof input === 'string' ? undefined : input)) so
provided init is preserved; update the super(...) call accordingly.

this.clerkUrl = this.deriveUrlFromHeaders(this);
this.cookies = this.parseCookies(this);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/react-router/src/server/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,14 +135,12 @@ export const wrapWithClerkState = (data: any) => {
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
// those carried by framework Request subclasses).
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,12 +45,14 @@ describe('patchRequest', () => {
expect(cloned.cache).toBe('no-cache');
});

// The previous "forwards signal aborts" regression test cannot run under Node
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
// AbortSignal throws TypeError due to undici's tightened cross-realm
// instanceof check. patchRequest intentionally omits the signal to avoid that
// error; verifying the trade-off in a unit test isn't possible in this
// environment.
it('forwards signal aborts from the original request', () => {
const controller = new AbortController();
const original = new Request('https://example.com/', { signal: controller.signal });
const cloned = patchRequest(original);
expect(cloned.signal.aborted).toBe(false);
controller.abort();
expect(cloned.signal.aborted).toBe(true);
});

it('clones POST requests without forwarding the body', () => {
// patchRequest deliberately omits `body` from the cloned init (see #7020)
Expand Down
7 changes: 1 addition & 6 deletions packages/tanstack-react-start/src/server/utils/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,17 +69,12 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
* @internal
*/
export const patchRequest = (request: Request) => {
// Omit `signal` from the clone: Node 24's bundled undici tightened the
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
// it does not recognize as its own — including the standard AbortSignal from
// framework Request subclasses or from `new AbortController()`. Until the
// ecosystem stabilizes, abort propagation through this clone is intentionally
// dropped. See packages/backend/src/proxy.ts for the same workaround.
const clonedRequest = new Request(request.url, {
headers: request.headers,
method: request.method,
redirect: request.redirect,
cache: request.cache,
signal: request.signal,
});

// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
Expand Down
Loading