diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchComponentSourceFromNetwork.spec.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchComponentSourceFromNetwork.spec.ts index 13f5012016..5b4733d130 100644 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchComponentSourceFromNetwork.spec.ts +++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchComponentSourceFromNetwork.spec.ts @@ -2,6 +2,7 @@ import { fetchComponentSourceFromNetwork } from '@/remote/worker/utils/fetchComp const COMPONENT_URL = 'https://api.twenty.com/rest/front-components/component-id/checksum-abc.js'; +const CACHE_BUSTED_COMPONENT_URL = `${COMPONENT_URL}?cacheBust=v2`; const PRESIGNED_URL = 'https://s3.example.com/component.js?signed=abc'; type FakeResponseInit = { @@ -52,7 +53,7 @@ describe('fetchComponentSourceFromNetwork', () => { }); expect(source).toBe('export default () => {};'); - expect(fetchMock).toHaveBeenCalledWith(COMPONENT_URL, { + expect(fetchMock).toHaveBeenCalledWith(CACHE_BUSTED_COMPONENT_URL, { headers: { Authorization: 'Bearer token' }, }); }); @@ -78,12 +79,29 @@ describe('fetchComponentSourceFromNetwork', () => { }); expect(source).toBe('presigned bundle source'); - expect(fetchMock).toHaveBeenNthCalledWith(1, COMPONENT_URL, { + expect(fetchMock).toHaveBeenNthCalledWith(1, CACHE_BUSTED_COMPONENT_URL, { headers: { Authorization: 'Bearer token' }, }); expect(fetchMock).toHaveBeenNthCalledWith(2, PRESIGNED_URL); }); + it('preserves existing query parameters when appending the cache bust parameter', async () => { + const fetchMock = jest.fn(async () => + createFakeResponse({ body: 'export default () => {};' }), + ); + + globalThis.fetch = fetchMock as unknown as typeof fetch; + + await fetchComponentSourceFromNetwork({ + url: `${COMPONENT_URL}?token=abc`, + }); + + expect(fetchMock).toHaveBeenCalledWith( + `${COMPONENT_URL}?token=abc&cacheBust=v2`, + { headers: undefined }, + ); + }); + it('throws when the initial response is not ok', async () => { const fetchMock = jest.fn(async () => createFakeResponse({ ok: false, status: 404, statusText: 'Not Found' }), diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchComponentSourceFromNetwork.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchComponentSourceFromNetwork.ts index afd8ce74a0..1d32fecfb3 100644 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchComponentSourceFromNetwork.ts +++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchComponentSourceFromNetwork.ts @@ -2,6 +2,26 @@ import { z } from 'zod'; const componentSourceHandoffSchema = z.object({ url: z.url() }); +// Changes the HTTP cache key so component responses cached before the +// front-component cache fix are bypassed instead of served corrupted. +const CACHE_BUST_QUERY_PARAMETER_NAME = 'cacheBust'; +const CACHE_BUST_QUERY_PARAMETER_VALUE = 'v2'; + +const appendCacheBustQueryParameter = (url: string): string => { + try { + const parsedUrl = new URL(url); + + parsedUrl.searchParams.set( + CACHE_BUST_QUERY_PARAMETER_NAME, + CACHE_BUST_QUERY_PARAMETER_VALUE, + ); + + return parsedUrl.toString(); + } catch { + return url; + } +}; + export const fetchComponentSourceFromNetwork = async ({ url, headers, @@ -9,7 +29,9 @@ export const fetchComponentSourceFromNetwork = async ({ url: string; headers?: Record; }): Promise => { - const response = await fetch(url, { headers }); + const response = await fetch(appendCacheBustQueryParameter(url), { + headers, + }); if (!response.ok) { throw new Error(