Bypass corrupted cached front-component responses with a cache-bust query parameter (#22854)
## Context Follow-up to #22672. Users' browsers hold corrupted cached responses for front-component request URLs from before the fix. #22672 fixed serving and caching for newly built front components, but the corrupted entries already sitting in browsers keep being served and need to be bypassed programmatically. ## What changed - `fetchComponentSourceFromNetwork` appends a constant `cacheBust=v2` query parameter to the component request (`GET /rest/front-components/:id/:cacheKey`). This changes the cache key, so any corrupted response cached under the old URL is never served again and the bundle is refetched. - Existing query parameters on the URL are preserved; if the URL cannot be parsed, the request falls back to the original URL unchanged. - The presigned S3 URL from the JSON handoff is left untouched: adding a query parameter there would invalidate its SigV4 signature. - The `CacheStorage` layer keeps using the logical component URL as its key, so its checksum-verified entries and hit behavior are unchanged. ## Test plan - `fetchComponentSourceFromNetwork.spec.ts`: assertions updated to expect the cache-busted component URL, plus a new test that existing query parameters are preserved and one that the presigned fetch stays unmodified; full renderer suite passes (226 tests). - `npx nx typecheck twenty-front-component-renderer` and `npx nx lint twenty-front-component-renderer` pass.
This commit is contained in:
+20
-2
@@ -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' }),
|
||||
|
||||
+23
-1
@@ -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<string, string>;
|
||||
}): Promise<string> => {
|
||||
const response = await fetch(url, { headers });
|
||||
const response = await fetch(appendCacheBustQueryParameter(url), {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user