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 | import { Jimp, JimpMime } from '../jimp.js'; | 8 | import { Jimp, JimpMime } from '../jimp.js'; |
| 9 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 9 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 10 | 10 | ||
| 11 | import { getConfigValue } from '../util.js'; | 11 | import { getConfigValue, invalidateFirefoxCache } from '../util.js'; |
| 12 | 12 | ||
| 13 | const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean'); | 13 | const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean'); |
| 14 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number')))); | 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 | 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 | } |
| 227 | 230 | ||
| @@ -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); |
| @@ -40,3 +40,13 @@ export function getRealIpFromHeader(req) { | |||
| 40 | 40 | ||
| 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 | */ | ||
| 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 | import sanitize from 'sanitize-filename'; | 16 | import sanitize from 'sanitize-filename'; |
| 17 | 17 | ||
| 18 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; | 18 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; |
| 19 | import { getConfigValue, color, delay, generateTimestamp } from './util.js'; | 19 | import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache } from './util.js'; |
| 20 | import { readSecret, writeSecret } from './endpoints/secrets.js'; | 20 | import { readSecret, writeSecret } from './endpoints/secrets.js'; |
| 21 | import { getContentOfType } from './endpoints/content-manager.js'; | 21 | import { getContentOfType } from './endpoints/content-manager.js'; |
| 22 | import { serverDirectory } from './server-directory.js'; | 22 | import { 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); |
| @@ -19,6 +19,7 @@ import chalk from 'chalk'; | |||
| 19 | import bytes from 'bytes'; | 19 | import bytes from 'bytes'; |
| 20 | import { LOG_LEVELS, CHAT_COMPLETION_SOURCES } from './constants.js'; | 20 | import { LOG_LEVELS, CHAT_COMPLETION_SOURCES } from './constants.js'; |
| 21 | import { serverDirectory } from './server-directory.js'; | 21 | import { serverDirectory } from './server-directory.js'; |
| 22 | import { isFirefox } from './express-common.js'; | ||
| 22 | 23 | ||
| 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 | */ | ||
| 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 | } | ||