Skip to content

Commit 395a4ca

Browse files
authored
chore(backend,nextjs): Fix ClerkRequest instance check and keyless custom headers (#7448)
1 parent 0d4cda7 commit 395a4ca

File tree

6 files changed

+53
-12
lines changed

6 files changed

+53
-12
lines changed

.changeset/dull-forks-agree.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

integration/templates/tanstack-react-start/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"@tanstack/react-start": "1.132.51",
1414
"react": "18.3.1",
1515
"react-dom": "18.3.1",
16-
"srvx": "0.8.15",
1716
"tailwind-merge": "^2.5.4"
1817
},
1918
"devDependencies": {

packages/backend/src/tokens/__tests__/clerkRequest.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,39 @@ describe('createClerkRequest', () => {
204204
expect(json.cookies).toBe('{}');
205205
});
206206
});
207+
208+
describe('duck typing detection (instanceof workaround)', () => {
209+
it('should create a new ClerkRequest from a regular Request', () => {
210+
const regularRequest = new Request('http://localhost:3000');
211+
const clerkRequest = createClerkRequest(regularRequest);
212+
213+
expect(clerkRequest).not.toBe(regularRequest);
214+
expect(clerkRequest.clerkUrl).toBeDefined();
215+
expect(clerkRequest.cookies).toBeDefined();
216+
});
217+
218+
it('should return an existing ClerkRequest instance unchanged', () => {
219+
const firstClerkRequest = createClerkRequest(new Request('http://localhost:3000'));
220+
const secondClerkRequest = createClerkRequest(firstClerkRequest);
221+
222+
expect(secondClerkRequest).toBe(firstClerkRequest);
223+
});
224+
225+
it('should work correctly with bundler-scoped Request classes', () => {
226+
// Simulate bundler creating a scoped Request class (like Request$1)
227+
class RequestScoped extends Request {
228+
constructor(input: RequestInfo | URL, init?: RequestInit) {
229+
super(input, init);
230+
}
231+
}
232+
233+
const scopedRequest = new RequestScoped('http://localhost:3000');
234+
const clerkRequest = createClerkRequest(scopedRequest);
235+
236+
// Should create a new ClerkRequest even though scopedRequest is a different Request class
237+
expect(clerkRequest).not.toBe(scopedRequest);
238+
expect(clerkRequest.clerkUrl).toBeDefined();
239+
expect(clerkRequest.cookies).toBeDefined();
240+
});
241+
});
207242
});

packages/backend/src/tokens/clerkRequest.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,12 @@ class ClerkRequest extends Request {
8282
}
8383

8484
export const createClerkRequest = (...args: ConstructorParameters<typeof ClerkRequest>): ClerkRequest => {
85-
return args[0] instanceof ClerkRequest ? args[0] : new ClerkRequest(...args);
85+
// Use duck typing instead of instanceof to avoid issues with polyfilled Request classes
86+
// (e.g., in TanStack Start or other environments with multiple Request class instances)
87+
// ClerkRequest has unique properties 'clerkUrl' and 'cookies' that distinguish it from Request
88+
const isClerkRequest = args[0] && typeof args[0] === 'object' && 'clerkUrl' in args[0] && 'cookies' in args[0];
89+
90+
return isClerkRequest ? (args[0] as ClerkRequest) : new ClerkRequest(...args);
8691
};
8792

8893
export type { ClerkRequest };

packages/nextjs/src/__tests__/keyless-custom-headers.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe('keyless-custom-headers', () => {
166166
});
167167

168168
describe('formatMetadataHeaders', () => {
169-
it('should format complete metadata object with all fields present', () => {
169+
it('should format complete metadata object with all fields present', async () => {
170170
const metadata = {
171171
nodeVersion: 'v18.17.0',
172172
nextVersion: 'next-server (v15.4.5)',
@@ -181,7 +181,7 @@ describe('keyless-custom-headers', () => {
181181
isCI: false,
182182
};
183183

184-
const result = formatMetadataHeaders(metadata);
184+
const result = await formatMetadataHeaders(metadata);
185185

186186
// Test exact header casing and values
187187
expect(result.get('Clerk-Node-Version')).toBe('v18.17.0');
@@ -196,7 +196,7 @@ describe('keyless-custom-headers', () => {
196196
expect(result.get('Clerk-Auth-Status')).toBe('signed-out');
197197
});
198198

199-
it('should handle missing optional fields gracefully', () => {
199+
it('should handle missing optional fields gracefully', async () => {
200200
const metadata = {
201201
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
202202
host: 'localhost:3000',
@@ -208,7 +208,7 @@ describe('keyless-custom-headers', () => {
208208
// Missing: nodeVersion, nextVersion, npmConfigUserAgent, port
209209
};
210210

211-
const result = formatMetadataHeaders(metadata);
211+
const result = await formatMetadataHeaders(metadata);
212212

213213
// Test that only present fields are set
214214
expect(result.get('Clerk-Client-User-Agent')).toBe('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
@@ -225,7 +225,7 @@ describe('keyless-custom-headers', () => {
225225
expect(result.get('Clerk-Node-Port')).toBeNull();
226226
});
227227

228-
it('should handle undefined values for optional fields', () => {
228+
it('should handle undefined values for optional fields', async () => {
229229
const metadata = {
230230
nodeVersion: undefined,
231231
nextVersion: undefined,
@@ -240,7 +240,7 @@ describe('keyless-custom-headers', () => {
240240
isCI: false,
241241
};
242242

243-
const result = formatMetadataHeaders(metadata);
243+
const result = await formatMetadataHeaders(metadata);
244244

245245
// Test that undefined fields are not set
246246
expect(result.get('Clerk-Node-Version')).toBeNull();
@@ -257,7 +257,7 @@ describe('keyless-custom-headers', () => {
257257
expect(result.get('Clerk-Auth-Status')).toBe('test-auth-status');
258258
});
259259

260-
it('should handle empty string values', () => {
260+
it('should handle empty string values', async () => {
261261
const metadata = {
262262
nodeVersion: '',
263263
nextVersion: '',
@@ -272,7 +272,7 @@ describe('keyless-custom-headers', () => {
272272
isCI: false,
273273
};
274274

275-
const result = formatMetadataHeaders(metadata);
275+
const result = await formatMetadataHeaders(metadata);
276276

277277
// Empty strings should not be set as headers
278278
expect(result.get('Clerk-Node-Version')).toBeNull();
@@ -513,7 +513,7 @@ describe('keyless-custom-headers', () => {
513513

514514
// Collect metadata and format headers
515515
const metadata = await collectKeylessMetadata();
516-
const headers = formatMetadataHeaders(metadata);
516+
const headers = await formatMetadataHeaders(metadata);
517517

518518
// Verify the full pipeline works correctly
519519
expect(headers.get('Clerk-Client-User-Agent')).toBe('Integration-Test-Agent');

packages/nextjs/src/server/keyless-custom-headers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function getNextVersion(): string | undefined {
9999
/**
100100
* Converts metadata to HTTP headers
101101
*/
102-
export function formatMetadataHeaders(metadata: MetadataHeaders): Headers {
102+
export async function formatMetadataHeaders(metadata: MetadataHeaders): Promise<Headers> {
103103
const headers = new Headers();
104104

105105
if (metadata.nodeVersion) {

0 commit comments

Comments
 (0)