fix #3199: firefox using cached images instead of new images. (#4743) * Fixed: https://github.com/SillyTavern/SillyTavern/issues/3199#issue-2745917391 * Wrap header set in isFirefox * Only invalidate Firefox caches. Also invalidate thumbnail caches. * Skip `mime.lookup` on non-Firefox browsers. * Improve param comments --------- Co-authored-by: user <user@exmaple.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

105b436a66df8d2520a7e1e659b4fb7e7249a98b

DeclineThyself <FallenHaze@tutamail.com>

Signed
4 files changed, +36 -2Ignore whitespace
src/endpoints/thumbnails.js+7 -1
@@ -8,7 +8,7 @@ import sanitize from 'sanitize-filename';
8import { Jimp, JimpMime } from '../jimp.js';8import { Jimp, JimpMime } from '../jimp.js';
9import { sync as writeFileAtomicSync } from 'write-file-atomic';9import { sync as writeFileAtomicSync } from 'write-file-atomic';
1010
11import { getConfigValue } from '../util.js';11import { getConfigValue, invalidateFirefoxCache } from '../util.js';
1212
13const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean');13const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean');
14const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number'))));14const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number'))));
@@ -222,6 +222,9 @@ router.get('/', async function (request, response) {
222 const contentType = mime.lookup(pathToOriginalFile) || 'image/png';222 const contentType = mime.lookup(pathToOriginalFile) || 'image/png';
223 const originalFile = await fsPromises.readFile(pathToOriginalFile);223 const originalFile = await fsPromises.readFile(pathToOriginalFile);
224 response.setHeader('Content-Type', contentType);224 response.setHeader('Content-Type', contentType);
225
226 invalidateFirefoxCache(pathToOriginalFile, request, response);
227
225 return response.send(originalFile);228 return response.send(originalFile);
226 }229 }
227230
@@ -238,6 +241,9 @@ router.get('/', async function (request, response) {
238 const contentType = mime.lookup(pathToCachedFile) || 'image/jpeg';241 const contentType = mime.lookup(pathToCachedFile) || 'image/jpeg';
239 const cachedFile = await fsPromises.readFile(pathToCachedFile);242 const cachedFile = await fsPromises.readFile(pathToCachedFile);
240 response.setHeader('Content-Type', contentType);243 response.setHeader('Content-Type', contentType);
244
245 invalidateFirefoxCache(file, request, response);
246
241 return response.send(cachedFile);247 return response.send(cachedFile);
242 } catch (error) {248 } catch (error) {
243 console.error('Failed getting thumbnail', error);249 console.error('Failed getting thumbnail', error);
src/express-common.js+10 -0
@@ -40,3 +40,13 @@ export function getRealIpFromHeader(req) {
4040
41 return getIpFromRequest(req);41 return getIpFromRequest(req);
42}42}
43
44/**
45 * Checks if the request is coming from a Firefox browser.
46 * @param {import('express').Request} req Request object
47 * @returns {boolean} True if the request is from Firefox, false otherwise.
48 */
49export function isFirefox(req) {
50 const userAgent = req.headers['user-agent'] || '';
51 return /firefox/i.test(userAgent);
52}
src/users.js+3 -1
@@ -16,7 +16,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
16import sanitize from 'sanitize-filename';16import sanitize from 'sanitize-filename';
1717
18import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js';18import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js';
19import { getConfigValue, color, delay, generateTimestamp } from './util.js';19import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache } from './util.js';
20import { readSecret, writeSecret } from './endpoints/secrets.js';20import { readSecret, writeSecret } from './endpoints/secrets.js';
21import { getContentOfType } from './endpoints/content-manager.js';21import { getContentOfType } from './endpoints/content-manager.js';
22import { serverDirectory } from './server-directory.js';22import { serverDirectory } from './server-directory.js';
@@ -952,6 +952,8 @@ function createRouteHandler(directoryFn) {
952 if (!exists) {952 if (!exists) {
953 return res.sendStatus(404);953 return res.sendStatus(404);
954 }954 }
955
956 invalidateFirefoxCache(filePath, req, res);
955 return res.sendFile(filePath, { root: directory });957 return res.sendFile(filePath, { root: directory });
956 } catch (error) {958 } catch (error) {
957 return res.sendStatus(500);959 return res.sendStatus(500);
src/util.js+16 -0
@@ -19,6 +19,7 @@ import chalk from 'chalk';
19import bytes from 'bytes';19import bytes from 'bytes';
20import { LOG_LEVELS, CHAT_COMPLETION_SOURCES } from './constants.js';20import { LOG_LEVELS, CHAT_COMPLETION_SOURCES } from './constants.js';
21import { serverDirectory } from './server-directory.js';21import { serverDirectory } from './server-directory.js';
22import { isFirefox } from './express-common.js';
2223
23/**24/**
24 * Parsed config object.25 * Parsed config object.
@@ -1295,3 +1296,18 @@ export function flattenSchema(schema, api) {
1295 delete flattenedSchema.$schema;1296 delete flattenedSchema.$schema;
1296 return flattenedSchema;1297 return flattenedSchema;
1297}1298}
1299
1300/**
1301 * If the file is an image, and the request's user agent matches Firefox, then the response's headers are set to invalidate the cache.
1302 * Without this, Firefox ignores updated images even after a refresh.
1303 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
1304 * @param {string} file File path
1305 * @param {import('express').Request} request Request object
1306 * @param {import('express').Response} response Response object
1307 */
1308export function invalidateFirefoxCache(file, request, response) {
1309 const mimeType = isFirefox(request) && mime.lookup(file);
1310 if (mimeType && mimeType.startsWith('image/')) {
1311 response.setHeader('Cache-Control', 'must-understand, no-store');
1312 }
1313}