Skip to content

Commit 097432d

Browse files
authored
fix(astro): Do not consume the request body in middleware (#9220)
1 parent 68ae308 commit 097432d

10 files changed

Lines changed: 42 additions & 73 deletions

File tree

.changeset/cyan-needles-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/astro": patch
3+
---
4+
5+
Fixed a bug where the `clerkMiddleware()` helper would consume the body of the request.

.changeset/eleven-corners-boil.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@clerk/astro": patch
3+
"@clerk/react-router": patch
4+
"@clerk/shared": patch
5+
"@clerk/tanstack-react-start": patch
6+
---
7+
8+
Moved the internal `patchRequest()` helper for reuse across framework SDKs.

packages/astro/src/server/clerk-middleware.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { htmlSafeJson } from '@clerk/shared/htmlSafeJson';
1919
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
2020
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
21+
import { patchRequest } from '@clerk/shared/patchRequest';
2122
import { isMalformedURLError } from '@clerk/shared/pathMatcher';
2223
import { isHttpOrHttps } from '@clerk/shared/proxy';
2324
import type { PendingSessionOptions } from '@clerk/shared/types';
@@ -83,7 +84,8 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
8384

8485
await initCloudflareEnv();
8586

86-
const clerkRequest = createClerkRequest(context.request);
87+
const patchedRequest = patchRequest(context.request);
88+
const clerkRequest = createClerkRequest(patchedRequest);
8789

8890
// Resolve keyless URLs per-request in development
8991
let keylessClaimUrl: string | undefined;

packages/react-router/src/server/clerkMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AuthObject } from '@clerk/backend';
22
import type { RequestState } from '@clerk/backend/internal';
33
import { AuthStatus, constants, createClerkRequest } from '@clerk/backend/internal';
44
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
5+
import { patchRequest } from '@clerk/shared/patchRequest';
56
import type { PendingSessionOptions } from '@clerk/shared/types';
67
import type { MiddlewareFunction } from 'react-router';
78
import { createContext } from 'react-router';
@@ -10,7 +11,6 @@ import { clerkClient } from './clerkClient';
1011
import { resolveKeysWithKeylessFallback } from './keyless/utils';
1112
import { loadOptions } from './loadOptions';
1213
import type { AdditionalStateOptions, ClerkMiddlewareOptions } from './types';
13-
import { patchRequest } from './utils';
1414

1515
type RequestStateContextValue = {
1616
requestState: RequestState<any>;

packages/react-router/src/server/loadOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createClerkRequest } from '@clerk/backend/internal';
22
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
33
import { getEnvVariable } from '@clerk/shared/getEnvVariable';
44
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
5+
import { patchRequest } from '@clerk/shared/patchRequest';
56
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
67
import { handleValueOrFn } from '@clerk/shared/utils';
78
import type { MiddlewareFunction } from 'react-router';
@@ -10,7 +11,6 @@ import { getPublicEnvVariables } from '../utils/env';
1011
import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../utils/errors';
1112
import { canUseKeyless } from '../utils/feature-flags';
1213
import type { ClerkMiddlewareOptions } from './types';
13-
import { patchRequest } from './utils';
1414

1515
export type DataFunctionArgs = Parameters<MiddlewareFunction<Response>>[0];
1616

packages/react-router/src/server/utils.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -141,29 +141,3 @@ export function getResponseClerkState(
141141
export const wrapWithClerkState = (data: any) => {
142142
return { clerkState: { __internal_clerk_state: { ...data } } };
143143
};
144-
145-
/**
146-
* Patches request to avoid duplex issues with unidici
147-
* For more information, see:
148-
* https://github.com/nodejs/node/issues/46221
149-
* https://github.com/whatwg/fetch/pull/1457
150-
* @internal
151-
*/
152-
export const patchRequest = (request: Request) => {
153-
// Omit `signal` from the clone: Node 24's bundled undici tightened the
154-
// instanceof AbortSignal check, which rejects cross-realm signals (e.g.
155-
// those carried by framework Request subclasses).
156-
const clonedRequest = new Request(request.url, {
157-
headers: request.headers,
158-
method: request.method,
159-
redirect: request.redirect,
160-
cache: request.cache,
161-
});
162-
163-
// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
164-
if (clonedRequest.method !== 'GET' && clonedRequest.body !== null && !('duplex' in clonedRequest)) {
165-
(clonedRequest as unknown as { duplex: 'half' }).duplex = 'half';
166-
}
167-
168-
return clonedRequest;
169-
};

packages/tanstack-react-start/src/__tests__/patchRequest.test.ts renamed to packages/shared/src/__tests__/patchRequest.spec.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { patchRequest } from '../server/utils';
3+
import { patchRequest } from '../patchRequest';
44

55
describe('patchRequest', () => {
66
it('preserves the URL including path and query string', () => {
@@ -10,9 +10,6 @@ describe('patchRequest', () => {
1010
});
1111

1212
it('preserves an encoded nested redirect_url with its own query and port', () => {
13-
// Mirrors the shape reported in the TanStack + Lovable handshake bug:
14-
// the outer URL's `redirect_url` param is a percent-encoded inner URL with
15-
// a port and its own query string, which must survive the clone verbatim.
1613
const nested = 'https://localhost:8080/?token=abc';
1714
const original = new Request(`https://example.com/handshake?redirect_url=${encodeURIComponent(nested)}`);
1815
const cloned = patchRequest(original);
@@ -45,17 +42,7 @@ describe('patchRequest', () => {
4542
expect(cloned.cache).toBe('no-cache');
4643
});
4744

48-
// The previous "forwards signal aborts" regression test cannot run under Node
49-
// 24 + jsdom + undici: constructing `new Request(url, { signal })` with any
50-
// AbortSignal throws TypeError due to undici's tightened cross-realm
51-
// instanceof check. patchRequest intentionally omits the signal to avoid that
52-
// error; verifying the trade-off in a unit test isn't possible in this
53-
// environment.
54-
5545
it('clones POST requests without forwarding the body', () => {
56-
// patchRequest deliberately omits `body` from the cloned init (see #7020)
57-
// so the original request's body stays intact for downstream consumers and
58-
// the undici duplex issues the helper was written to avoid do not resurface.
5946
const original = new Request('https://example.com/api', {
6047
method: 'POST',
6148
body: 'payload',
@@ -64,5 +51,6 @@ describe('patchRequest', () => {
6451
const cloned = patchRequest(original);
6552
expect(cloned.method).toBe('POST');
6653
expect(cloned.body).toBeNull();
54+
expect(original.bodyUsed).toBe(false);
6755
});
6856
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Clones a request without its body or signal for authentication.
3+
*
4+
* @internal
5+
*/
6+
export const patchRequest = (request: Request) => {
7+
// Node's bundled undici rejects cross-realm signals from framework Request subclasses.
8+
const clonedRequest = new Request(request.url, {
9+
headers: request.headers,
10+
method: request.method,
11+
redirect: request.redirect,
12+
cache: request.cache,
13+
});
14+
15+
if (clonedRequest.method !== 'GET' && clonedRequest.body !== null && !('duplex' in clonedRequest)) {
16+
(clonedRequest as unknown as { duplex: 'half' }).duplex = 'half';
17+
}
18+
19+
return clonedRequest;
20+
};

packages/tanstack-react-start/src/server/clerkMiddleware.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RequestState } from '@clerk/backend/internal';
22
import { AuthStatus, constants, createClerkRequest } from '@clerk/backend/internal';
33
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
4+
import { patchRequest } from '@clerk/shared/patchRequest';
45
import type { PendingSessionOptions } from '@clerk/shared/types';
56
import type { AnyRequestMiddleware } from '@tanstack/react-start';
67
import { createMiddleware } from '@tanstack/react-start';
@@ -10,7 +11,7 @@ import { clerkClient } from './clerkClient';
1011
import { resolveKeysWithKeylessFallback } from './keyless/utils';
1112
import { loadOptions } from './loadOptions';
1213
import type { ClerkMiddlewareOptions, ClerkMiddlewareOptionsCallback } from './types';
13-
import { getResponseClerkState, patchRequest } from './utils';
14+
import { getResponseClerkState } from './utils';
1415

1516
export const clerkMiddleware = (
1617
options?: ClerkMiddlewareOptions | ClerkMiddlewareOptionsCallback,

packages/tanstack-react-start/src/server/utils/index.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -69,32 +69,3 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
6969

7070
return clerkInitialState;
7171
}
72-
73-
/**
74-
* Patches request to avoid duplex issues with unidici
75-
* For more information, see:
76-
* https://github.com/nodejs/node/issues/46221
77-
* https://github.com/whatwg/fetch/pull/1457
78-
* @internal
79-
*/
80-
export const patchRequest = (request: Request) => {
81-
// Omit `signal` from the clone: Node 24's bundled undici tightened the
82-
// instanceof AbortSignal check on RequestInit.signal and rejects any signal
83-
// it does not recognize as its own — including the standard AbortSignal from
84-
// framework Request subclasses or from `new AbortController()`. Until the
85-
// ecosystem stabilizes, abort propagation through this clone is intentionally
86-
// dropped. See packages/backend/src/proxy.ts for the same workaround.
87-
const clonedRequest = new Request(request.url, {
88-
headers: request.headers,
89-
method: request.method,
90-
redirect: request.redirect,
91-
cache: request.cache,
92-
});
93-
94-
// If duplex is not set, set it to 'half' to avoid duplex issues with unidici
95-
if (clonedRequest.method !== 'GET' && clonedRequest.body !== null && !('duplex' in clonedRequest)) {
96-
(clonedRequest as unknown as { duplex: 'half' }).duplex = 'half';
97-
}
98-
99-
return clonedRequest;
100-
};

0 commit comments

Comments
 (0)