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>
Signed| @@ -8,7 +8,7 @@ import sanitize from 'sanitize-filename'; | ||
| 8 | 8 | import { Jimp, JimpMime } from '../jimp.js'; |
| 9 | 9 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 10 | 10 | |
| 11 | 11 | import { getConfigValue, invalidateFirefoxCache } from '../util.js'; |
| 12 | 12 | |
| 13 | 13 | const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean'); |
| 14 | 14 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number')))); |
| @@ -222,6 +222,9 @@ router.get('/', async function (request, response) { | ||
| 222 | 222 | const contentType = mime.lookup(pathToOriginalFile) || 'image/png'; |
| 223 | 223 | const originalFile = await fsPromises.readFile(pathToOriginalFile); |
| 224 | 224 | response.setHeader('Content-Type', contentType); |
| 225 | + | |
| 226 | + invalidateFirefoxCache(pathToOriginalFile, request, response); | |
| 227 | + | |
| 225 | 228 | return response.send(originalFile); |
| 226 | 229 | } |
| 227 | 230 | |
| @@ -238,6 +241,9 @@ router.get('/', async function (request, response) { | ||
| 238 | 241 | const contentType = mime.lookup(pathToCachedFile) || 'image/jpeg'; |
| 239 | 242 | const cachedFile = await fsPromises.readFile(pathToCachedFile); |
| 240 | 243 | response.setHeader('Content-Type', contentType); |
| 244 | + | |
| 245 | + invalidateFirefoxCache(file, request, response); | |
| 246 | + | |
| 241 | 247 | return response.send(cachedFile); |
| 242 | 248 | } catch (error) { |
| 243 | 249 | console.error('Failed getting thumbnail', error); |
| @@ -40,3 +40,13 @@ export function getRealIpFromHeader(req) { | ||
| 40 | 40 | |
| 41 | 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 | + */ | |
| 49 | +export function isFirefox(req) { | |
| 50 | + const userAgent = req.headers['user-agent'] || ''; | |
| 51 | + return /firefox/i.test(userAgent); | |
| 52 | +} | |
| @@ -16,7 +16,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | ||
| 16 | 16 | import sanitize from 'sanitize-filename'; |
| 17 | 17 | |
| 18 | 18 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; |
| 19 | 19 | import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache } from './util.js'; |
| 20 | 20 | import { readSecret, writeSecret } from './endpoints/secrets.js'; |
| 21 | 21 | import { getContentOfType } from './endpoints/content-manager.js'; |
| 22 | 22 | import { serverDirectory } from './server-directory.js'; |
| @@ -952,6 +952,8 @@ function createRouteHandler(directoryFn) { | ||
| 952 | 952 | if (!exists) { |
| 953 | 953 | return res.sendStatus(404); |
| 954 | 954 | } |
| 955 | + | |
| 956 | + invalidateFirefoxCache(filePath, req, res); | |
| 955 | 957 | return res.sendFile(filePath, { root: directory }); |
| 956 | 958 | } catch (error) { |
| 957 | 959 | return res.sendStatus(500); |
| @@ -19,6 +19,7 @@ import chalk from 'chalk'; | ||
| 19 | 19 | import bytes from 'bytes'; |
| 20 | 20 | import { LOG_LEVELS, CHAT_COMPLETION_SOURCES } from './constants.js'; |
| 21 | 21 | import { serverDirectory } from './server-directory.js'; |
| 22 | +import { isFirefox } from './express-common.js'; | |
| 22 | 23 | |
| 23 | 24 | /** |
| 24 | 25 | * Parsed config object. |
| @@ -1295,3 +1296,18 @@ export function flattenSchema(schema, api) { | ||
| 1295 | 1296 | delete flattenedSchema.$schema; |
| 1296 | 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 | + */ | |
| 1308 | +export 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 | +} | |