mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-15 23:43:06 +03:00
⚙️ refactor: brotli asset serving behind a feature toggle (#13641)
This commit is contained in:
@@ -833,6 +833,9 @@ ALLOW_SHARED_LINKS_PUBLIC=false
|
||||
# If you have another service in front of your LibreChat doing compression, disable express based compression here
|
||||
# DISABLE_COMPRESSION=true
|
||||
|
||||
# Serve precompressed Brotli versions of static app assets when available.
|
||||
# ENABLE_STATIC_ASSET_BROTLI=true
|
||||
|
||||
# If you have gzipped version of uploaded image images in the same folder, this will enable gzip scan and serving of these images
|
||||
# Note: The images folder will be scanned on startup and a ma kept in memory. Be careful for large number of images.
|
||||
# ENABLE_IMAGE_OUTPUT_GZIP_SCAN=true
|
||||
|
||||
@@ -5,6 +5,12 @@ const request = require('supertest');
|
||||
const zlib = require('zlib');
|
||||
const staticCache = require('../staticCache');
|
||||
|
||||
const binaryParser = (res, callback) => {
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => callback(null, Buffer.concat(chunks)));
|
||||
};
|
||||
|
||||
describe('staticCache', () => {
|
||||
let app;
|
||||
let testDir;
|
||||
@@ -36,10 +42,15 @@ describe('staticCache', () => {
|
||||
fs.writeFileSync(manifestFile, jsonContent);
|
||||
fs.writeFileSync(swFile, swContent);
|
||||
|
||||
// Create gzipped versions of some files
|
||||
// Create precompressed versions of some files
|
||||
fs.writeFileSync(testFile + '.gz', zlib.gzipSync(jsContent));
|
||||
fs.writeFileSync(testFile + '.br', zlib.brotliCompressSync(jsContent));
|
||||
fs.writeFileSync(path.join(testDir, 'test.css'), 'body { color: red; }');
|
||||
fs.writeFileSync(path.join(testDir, 'test.css.gz'), zlib.gzipSync('body { color: red; }'));
|
||||
fs.writeFileSync(
|
||||
path.join(testDir, 'test.css.br'),
|
||||
zlib.brotliCompressSync('body { color: red; }'),
|
||||
);
|
||||
|
||||
// Create a file that only exists in gzipped form
|
||||
fs.writeFileSync(
|
||||
@@ -67,6 +78,7 @@ describe('staticCache', () => {
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.STATIC_CACHE_S_MAX_AGE;
|
||||
delete process.env.STATIC_CACHE_MAX_AGE;
|
||||
delete process.env.ENABLE_STATIC_ASSET_BROTLI;
|
||||
});
|
||||
describe('cache headers in production', () => {
|
||||
beforeEach(() => {
|
||||
@@ -193,6 +205,51 @@ describe('staticCache', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
});
|
||||
|
||||
it('should serve Brotli files when client accepts Brotli encoding', async () => {
|
||||
process.env.ENABLE_STATIC_ASSET_BROTLI = 'true';
|
||||
app.use(staticCache(testDir, { skipGzipScan: false }));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/test.js')
|
||||
.set('Accept-Encoding', 'br, gzip, deflate')
|
||||
.buffer(true)
|
||||
.parse(binaryParser)
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers['content-encoding']).toBe('br');
|
||||
expect(response.headers['content-type']).toMatch(/javascript/);
|
||||
expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=86400');
|
||||
expect(zlib.brotliDecompressSync(response.body).toString()).toBe('console.log("test");');
|
||||
});
|
||||
|
||||
it('should prefer Brotli over gzip when both encodings are accepted', async () => {
|
||||
process.env.ENABLE_STATIC_ASSET_BROTLI = 'true';
|
||||
app.use(staticCache(testDir, { skipGzipScan: false }));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/test.css')
|
||||
.set('Accept-Encoding', 'gzip, br')
|
||||
.buffer(true)
|
||||
.parse(binaryParser)
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers['content-encoding']).toBe('br');
|
||||
expect(response.headers['content-type']).toMatch(/css/);
|
||||
expect(zlib.brotliDecompressSync(response.body).toString()).toBe('body { color: red; }');
|
||||
});
|
||||
|
||||
it('should keep serving gzip when Brotli is not enabled', async () => {
|
||||
app.use(staticCache(testDir, { skipGzipScan: false }));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/test.js')
|
||||
.set('Accept-Encoding', 'br, gzip, deflate')
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers['content-encoding']).toBe('gzip');
|
||||
expect(response.text).toBe('console.log("test");');
|
||||
});
|
||||
|
||||
it('should serve gzipped files when client accepts gzip encoding', async () => {
|
||||
app.use(staticCache(testDir, { skipGzipScan: false }));
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ const oneDayInSeconds = 24 * 60 * 60;
|
||||
|
||||
const sMaxAge = process.env.STATIC_CACHE_S_MAX_AGE || oneDayInSeconds;
|
||||
const maxAge = process.env.STATIC_CACHE_MAX_AGE || oneDayInSeconds * 2;
|
||||
const isEnabled = (value) => value === true || String(value).toLowerCase() === 'true';
|
||||
|
||||
/**
|
||||
* Creates an Express static middleware with optional gzip compression and configurable caching
|
||||
* Creates an Express static middleware with optional precompressed asset serving and configurable caching
|
||||
*
|
||||
* @param {string} staticPath - The file system path to serve static files from
|
||||
* @param {Object} [options={}] - Configuration options
|
||||
@@ -18,6 +19,7 @@ const maxAge = process.env.STATIC_CACHE_MAX_AGE || oneDayInSeconds * 2;
|
||||
*/
|
||||
function staticCache(staticPath, options = {}) {
|
||||
const { noCache = false, skipGzipScan = false } = options;
|
||||
const enableBrotli = isEnabled(process.env.ENABLE_STATIC_ASSET_BROTLI);
|
||||
|
||||
const setHeaders = (res, filePath) => {
|
||||
if (process.env.NODE_ENV?.toLowerCase() !== 'production') {
|
||||
@@ -51,8 +53,8 @@ function staticCache(staticPath, options = {}) {
|
||||
});
|
||||
} else {
|
||||
return expressStaticGzip(staticPath, {
|
||||
enableBrotli: false,
|
||||
orderPreference: ['gz'],
|
||||
enableBrotli,
|
||||
orderPreference: enableBrotli ? ['br', 'gz'] : ['gz'],
|
||||
setHeaders,
|
||||
index: false,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user