Skip to content

Commit 02ce6eb

Browse files
committed
Merge branch 'main' into feat/seat-based-billing
2 parents dc73582 + 1cf76d1 commit 02ce6eb

8 files changed

Lines changed: 94 additions & 8 deletions

File tree

.changeset/beige-regions-join.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/ui': patch
3+
---
4+
5+
Move `react` and `react-dom` from `dependencies` to `peerDependencies`
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/expo': patch
3+
---
4+
5+
Add `appleSignIn` option to the Expo config plugin. Setting `appleSignIn: false` prevents the Sign in with Apple entitlement from being added unconditionally, allowing apps that do not use Apple Sign In to opt out.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': patch
3+
---
4+
5+
Fix `ERR_CONTENT_DECODING_FAILED` when loading proxied assets by requesting uncompressed responses from FAPI and stripping `Content-Encoding`/`Content-Length` headers that `fetch()` invalidates through auto-decompression.

integration/templates/next-cache-components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"@types/node": "^18.19.33",
1414
"@types/react": "^19.0.0",
1515
"@types/react-dom": "^19.0.0",
16-
"next": "16.1.6",
16+
"next": "^16.2.1",
1717
"react": "^19.0.0",
1818
"react-dom": "^19.0.0",
1919
"typescript": "^5.7.3"

packages/backend/src/__tests__/proxy.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,50 @@ describe('proxy', () => {
481481
expect(response.headers.get('Location')).toBe('https://accounts.google.com/oauth/authorize');
482482
});
483483

484+
it('sets Accept-Encoding to identity to avoid double compression', async () => {
485+
const mockResponse = new Response(JSON.stringify({ client: {} }), {
486+
status: 200,
487+
headers: { 'Content-Type': 'application/json' },
488+
});
489+
mockFetch.mockResolvedValue(mockResponse);
490+
491+
const request = new Request('https://example.com/__clerk/v1/client', {
492+
headers: { 'Accept-Encoding': 'gzip, deflate, br' },
493+
});
494+
495+
await clerkFrontendApiProxy(request, {
496+
publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k',
497+
secretKey: 'sk_test_xxx',
498+
});
499+
500+
const [, options] = mockFetch.mock.calls[0];
501+
expect(options.headers.get('Accept-Encoding')).toBe('identity');
502+
});
503+
504+
it('strips Content-Encoding and Content-Length from response even if upstream ignores identity', async () => {
505+
// Upstream may ignore Accept-Encoding: identity and compress anyway
506+
const mockResponse = new Response('decoded body', {
507+
status: 200,
508+
headers: {
509+
'Content-Type': 'application/javascript',
510+
'Content-Encoding': 'gzip',
511+
'Content-Length': '500',
512+
},
513+
});
514+
mockFetch.mockResolvedValue(mockResponse);
515+
516+
const request = new Request('https://example.com/__clerk/v1/client');
517+
518+
const response = await clerkFrontendApiProxy(request, {
519+
publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k',
520+
secretKey: 'sk_test_xxx',
521+
});
522+
523+
expect(response.headers.has('Content-Encoding')).toBe(false);
524+
expect(response.headers.has('Content-Length')).toBe(false);
525+
expect(response.headers.get('Content-Type')).toBe('application/javascript');
526+
});
527+
484528
it('preserves relative Location headers', async () => {
485529
const mockResponse = new Response(null, {
486530
status: 302,

packages/backend/src/proxy.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ const HOP_BY_HOP_HEADERS = [
5454
'upgrade',
5555
];
5656

57+
// Headers to strip from proxied responses. fetch() auto-decompresses
58+
// response bodies, so Content-Encoding no longer describes the body
59+
// and Content-Length reflects the compressed size. We request identity
60+
// encoding upstream to avoid the double compression pass, but strip
61+
// these defensively since servers may ignore Accept-Encoding: identity.
62+
const RESPONSE_HEADERS_TO_STRIP = ['content-encoding', 'content-length'];
63+
5764
/**
5865
* Derives the Frontend API URL from a publishable key.
5966
* @param publishableKey - The Clerk publishable key
@@ -235,6 +242,11 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend
235242
const fapiHost = new URL(fapiBaseUrl).host;
236243
headers.set('Host', fapiHost);
237244

245+
// Request uncompressed responses to avoid a double compression pass.
246+
// fetch() auto-decompresses, so without this FAPI compresses → fetch
247+
// decompresses → the serving layer re-compresses for the browser.
248+
headers.set('Accept-Encoding', 'identity');
249+
238250
// Set X-Forwarded-* headers for proxy awareness
239251
// Only set these if not already present (preserve values from upstream proxies)
240252
if (!headers.has('X-Forwarded-Host')) {
@@ -271,10 +283,11 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend
271283

272284
const response = await fetch(targetUrl.toString(), fetchOptions);
273285

274-
// Build response headers, excluding hop-by-hop headers
286+
// Build response headers, excluding hop-by-hop and encoding headers
275287
const responseHeaders = new Headers();
276288
response.headers.forEach((value, key) => {
277-
if (!HOP_BY_HOP_HEADERS.includes(key.toLowerCase())) {
289+
const lower = key.toLowerCase();
290+
if (!HOP_BY_HOP_HEADERS.includes(lower) && !RESPONSE_HEADERS_TO_STRIP.includes(lower)) {
278291
responseHeaders.set(key, value);
279292
}
280293
});
@@ -295,11 +308,20 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend
295308
}
296309
}
297310

298-
return new Response(response.body, {
311+
const proxyResponse = new Response(response.body, {
299312
status: response.status,
300313
statusText: response.statusText,
301314
headers: responseHeaders,
302315
});
316+
317+
// Some runtimes may re-add Content-Length when constructing the Response.
318+
// Delete explicitly since fetch() decoded the body and the original values
319+
// no longer reflect the actual content.
320+
for (const header of RESPONSE_HEADERS_TO_STRIP) {
321+
proxyResponse.headers.delete(header);
322+
}
323+
324+
return proxyResponse;
303325
} catch (error) {
304326
const message = error instanceof Error ? error.message : 'Unknown error';
305327
return createErrorResponse('proxy_request_failed', `Failed to proxy request to Clerk FAPI: ${message}`, 502);

packages/expo/app.plugin.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,8 +586,11 @@ const withClerkAppleSignIn = config => {
586586
};
587587

588588
const withClerkExpo = (config, props = {}) => {
589+
const { appleSignIn = true } = props;
589590
config = withClerkIOS(config);
590-
config = withClerkAppleSignIn(config);
591+
if (appleSignIn !== false) {
592+
config = withClerkAppleSignIn(config);
593+
}
591594
config = withClerkGoogleSignIn(config);
592595
config = withClerkAndroid(config);
593596
config = withClerkKeychainService(config, props);

packages/ui/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@
108108
"csstype": "3.1.3",
109109
"dequal": "2.0.3",
110110
"input-otp": "1.4.2",
111-
"qrcode.react": "4.2.0",
112-
"react": "catalog:peer-react",
113-
"react-dom": "catalog:peer-react"
111+
"qrcode.react": "4.2.0"
114112
},
115113
"devDependencies": {
116114
"@floating-ui/react-dom": "^2.1.6",
@@ -127,6 +125,10 @@
127125
"tsdown": "catalog:repo",
128126
"webpack-merge": "^5.10.0"
129127
},
128+
"peerDependencies": {
129+
"react": "catalog:peer-react",
130+
"react-dom": "catalog:peer-react"
131+
},
130132
"engines": {
131133
"node": ">=20.9.0"
132134
},

0 commit comments

Comments
 (0)