diff --git a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts index 24e549cc86..325bfb5c7d 100644 --- a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts @@ -92,7 +92,7 @@ export class FileController { return res.redirect(fileResponse.presignedUrl); } - setFileResponseHeaders(res, fileResponse.mimeType); + setFileResponseHeaders(res, fileResponse.mimeType, FileFolder.PublicAsset); try { await pipeline(fileResponse.stream, res); diff --git a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts index d2dbcdf07e..9df2b56095 100644 --- a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts @@ -8,62 +8,64 @@ registerEnumType(FileFolder, { export type FileFolderConfig = { ignoreExpirationToken: boolean; - immutable: boolean; + cacheControl: string | null; }; export const IMMUTABLE_FILE_CACHE_CONTROL = 'private, max-age=86400, immutable'; +export const PUBLIC_ASSET_CACHE_CONTROL = 'public, max-age=3600'; + export const fileFolderConfigs: Record = { [FileFolder.CorePicture]: { ignoreExpirationToken: true, - immutable: true, + cacheControl: IMMUTABLE_FILE_CACHE_CONTROL, }, [FileFolder.AgentChat]: { ignoreExpirationToken: false, - immutable: true, + cacheControl: IMMUTABLE_FILE_CACHE_CONTROL, }, [FileFolder.BuiltLogicFunction]: { ignoreExpirationToken: false, - immutable: false, + cacheControl: null, }, [FileFolder.BuiltFrontComponent]: { ignoreExpirationToken: false, - immutable: false, + cacheControl: IMMUTABLE_FILE_CACHE_CONTROL, }, [FileFolder.PublicAsset]: { ignoreExpirationToken: true, - immutable: false, + cacheControl: PUBLIC_ASSET_CACHE_CONTROL, }, [FileFolder.Source]: { ignoreExpirationToken: false, - immutable: false, + cacheControl: null, }, [FileFolder.FilesField]: { ignoreExpirationToken: false, - immutable: true, + cacheControl: IMMUTABLE_FILE_CACHE_CONTROL, }, [FileFolder.Dependencies]: { ignoreExpirationToken: false, - immutable: false, + cacheControl: null, }, [FileFolder.Workflow]: { ignoreExpirationToken: false, - immutable: true, + cacheControl: IMMUTABLE_FILE_CACHE_CONTROL, }, [FileFolder.EmailAttachment]: { ignoreExpirationToken: false, - immutable: true, + cacheControl: IMMUTABLE_FILE_CACHE_CONTROL, }, [FileFolder.AppTarball]: { ignoreExpirationToken: false, - immutable: false, + cacheControl: null, }, [FileFolder.GeneratedSdkClient]: { ignoreExpirationToken: false, - immutable: false, + cacheControl: null, }, [FileFolder.Dpa]: { ignoreExpirationToken: false, - immutable: true, + cacheControl: IMMUTABLE_FILE_CACHE_CONTROL, }, }; diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts index 314303d8b5..ab6c8fc597 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts @@ -13,10 +13,7 @@ import { FileStorageExceptionCode, } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { - fileFolderConfigs, - IMMUTABLE_FILE_CACHE_CONTROL, -} from 'src/engine/core-modules/file/interfaces/file-folder.interface'; +import { fileFolderConfigs } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; import { type FileResponse } from 'src/engine/core-modules/file/types/file-response.type'; import { FILE_STATUS } from 'src/engine/core-modules/file/types/file-status.types'; import { getContentDisposition } from 'src/engine/core-modules/file/utils/get-content-disposition.utils'; @@ -217,9 +214,8 @@ export class FileService { ), responseContentType: mimeType, responseContentDisposition: getContentDisposition(mimeType), - responseCacheControl: fileFolderConfigs[fileFolder].immutable - ? IMMUTABLE_FILE_CACHE_CONTROL - : undefined, + responseCacheControl: + fileFolderConfigs[fileFolder].cacheControl ?? undefined, }); if (presignedUrl) { diff --git a/packages/twenty-server/src/engine/core-modules/file/utils/__tests__/set-file-response-headers.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/file/utils/__tests__/set-file-response-headers.utils.spec.ts index a2db42b2b3..e95de967bc 100644 --- a/packages/twenty-server/src/engine/core-modules/file/utils/__tests__/set-file-response-headers.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/file/utils/__tests__/set-file-response-headers.utils.spec.ts @@ -93,6 +93,7 @@ describe('setFileResponseHeaders', () => { FileFolder.AgentChat, FileFolder.EmailAttachment, FileFolder.Dpa, + FileFolder.BuiltFrontComponent, ])( 'should set an immutable Cache-Control for immutable folder %s', (fileFolder) => { @@ -107,21 +108,36 @@ describe('setFileResponseHeaders', () => { }, ); - it.each([ - FileFolder.PublicAsset, - FileFolder.AppTarball, - FileFolder.Source, - FileFolder.BuiltFrontComponent, - ])('should not set Cache-Control for mutable folder %s', (fileFolder) => { + it('should set a bounded public Cache-Control for the PublicAsset folder', () => { const res = createMockResponse(); - setFileResponseHeaders(res as any, 'image/png', fileFolder); + setFileResponseHeaders(res as any, 'image/png', FileFolder.PublicAsset); - expect(res.setHeader).not.toHaveBeenCalledWith( + expect(res.setHeader).toHaveBeenCalledWith( 'Cache-Control', - expect.anything(), + 'public, max-age=3600', ); }); + + it.each([ + FileFolder.AppTarball, + FileFolder.Source, + FileFolder.BuiltLogicFunction, + FileFolder.Dependencies, + FileFolder.GeneratedSdkClient, + ])( + 'should not set Cache-Control for non-cacheable folder %s', + (fileFolder) => { + const res = createMockResponse(); + + setFileResponseHeaders(res as any, 'image/png', fileFolder); + + expect(res.setHeader).not.toHaveBeenCalledWith( + 'Cache-Control', + expect.anything(), + ); + }, + ); }); describe('getContentDisposition', () => { diff --git a/packages/twenty-server/src/engine/core-modules/file/utils/set-file-response-headers.utils.ts b/packages/twenty-server/src/engine/core-modules/file/utils/set-file-response-headers.utils.ts index c87be51b17..0a5323be86 100644 --- a/packages/twenty-server/src/engine/core-modules/file/utils/set-file-response-headers.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/file/utils/set-file-response-headers.utils.ts @@ -3,10 +3,7 @@ import { type Response } from 'express'; import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { - fileFolderConfigs, - IMMUTABLE_FILE_CACHE_CONTROL, -} from 'src/engine/core-modules/file/interfaces/file-folder.interface'; +import { fileFolderConfigs } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; import { getContentDisposition } from 'src/engine/core-modules/file/utils/get-content-disposition.utils'; export const setFileResponseHeaders = ( @@ -20,7 +17,11 @@ export const setFileResponseHeaders = ( res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Content-Disposition', getContentDisposition(contentType)); - if (isDefined(fileFolder) && fileFolderConfigs[fileFolder].immutable) { - res.setHeader('Cache-Control', IMMUTABLE_FILE_CACHE_CONTROL); + const cacheControl = isDefined(fileFolder) + ? fileFolderConfigs[fileFolder].cacheControl + : null; + + if (isDefined(cacheControl)) { + res.setHeader('Cache-Control', cacheControl); } }; diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts index c87e54d68c..410493cac7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/controllers/front-component.controller.ts @@ -11,6 +11,7 @@ import { import { pipeline } from 'stream/promises'; import { Response } from 'express'; +import { FileFolder } from 'twenty-shared/types'; import { FileStorageException, @@ -87,7 +88,11 @@ export class FrontComponentController { return res.redirect(fileResponse.presignedUrl); } - setFileResponseHeaders(res, fileResponse.mimeType); + setFileResponseHeaders( + res, + fileResponse.mimeType, + FileFolder.BuiltFrontComponent, + ); try { await pipeline(fileResponse.stream, res); diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts index 50c064d357..10d728a417 100644 --- a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts @@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { fileFolderConfigs } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; import { type FileResponse } from 'src/engine/core-modules/file/types/file-response.type'; import { getContentDisposition } from 'src/engine/core-modules/file/utils/get-content-disposition.utils'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -332,6 +333,9 @@ export class FrontComponentService { ), responseContentType: mimeType, responseContentDisposition: getContentDisposition(mimeType), + responseCacheControl: + fileFolderConfigs[FileFolder.BuiltFrontComponent].cacheControl ?? + undefined, }); if (presignedUrl) {