mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-05 04:06:37 +02:00
fix(settings): derive randomVersionString from release identity (#7563)
* fix(settings): derive randomVersionString from release identity Fixes #7213. Etherpad appends a `?v=<token>` cache-buster to static assets and embeds the same token as `clientVars.randomVersionString` in the padbootstrap JS bundle produced by specialpages.ts. Because esbuild's content-hash feeds back into the generated bundle filename (`padbootstrap-<hash>.min.js`), the token's value determines the file that clients are told to load. Historically the token was `randomString(4)`, regenerated on every boot. In a horizontally-scaled deployment (ingress → etherpad service → multiple pods) that meant every pod produced a different filename for the same built artifact. A client that loaded the HTML from pod A would request `padbootstrap-ABCD.min.js` from pod B and hit a 404 when the upstream balancer placed the follow-up request elsewhere. Derive the token deterministically so pods of the same build emit identical filenames, while still rotating on release so clients invalidate their cache correctly: ETHERPAD_VERSION_STRING env → verbatim (integrator override) else → sha256(version + "|" + gitVersion)[:8] Backwards-compatible: single-pod deployments see the same effective behavior (token rotates each release). Integrators who want to pin the token explicitly — e.g. tying it to their own deploy ID — can set `ETHERPAD_VERSION_STRING` in the environment. Test coverage added in src/tests/backend/specs/settings.ts: - Default shape is an 8-hex-char sha256 prefix. - ETHERPAD_VERSION_STRING override is respected verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7213): call reloadSettings() to exercise ETHERPAD_VERSION_STRING The token is assigned inside reloadSettings, not parseSettings, so a parseSettings-only call never sees the env var. Drive reloadSettings directly, restoring the file paths and the prior token afterwards so other tests see a clean module state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
547af5c2f0
commit
d619f03214
@ -37,6 +37,7 @@ import path from 'node:path';
|
||||
import {argv} from './Cli'
|
||||
import jsonminify from 'jsonminify';
|
||||
import log4js from 'log4js';
|
||||
import {createHash} from 'node:crypto';
|
||||
import randomString from './randomstring';
|
||||
const suppressDisableMsg = ' -- To suppress these warning messages change ' +
|
||||
'suppressErrorsInPadText to true in your settings.json\n';
|
||||
@ -1077,18 +1078,38 @@ export const reloadSettings = () => {
|
||||
}
|
||||
|
||||
/*
|
||||
* At each start, Etherpad generates a random string and appends it as query
|
||||
* parameter to the URLs of the static assets, in order to force their reload.
|
||||
* Subsequent requests will be cached, as long as the server is not reloaded.
|
||||
* Etherpad appends this token as a ?v= query parameter on static assets
|
||||
* and as the content seed for the padbootstrap-<hash>.min.js bundles, so
|
||||
* clients invalidate their cache when a release goes out.
|
||||
*
|
||||
* For the rationale behind this choice, see
|
||||
* https://github.com/ether/etherpad-lite/pull/3958
|
||||
* Historically this was `randomString(4)`, regenerated on every boot. That
|
||||
* broke horizontally-scaled deployments (multi-pod behind an ingress):
|
||||
* every pod hashed the bootstrap bundle with its own seed, so an HTML
|
||||
* response from pod A referenced `padbootstrap-ABCD.min.js` while pod B
|
||||
* only served `padbootstrap-WXYZ.min.js`, producing 404s on any cross-pod
|
||||
* request (issue #7213).
|
||||
*
|
||||
* ACHTUNG: this may prevent caching HTTP proxies to work
|
||||
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
|
||||
* Derive the token deterministically from the Etherpad version and
|
||||
* whatever git SHA is available. Pods that ship the same artifact now
|
||||
* produce the same hash, and the token still rotates per release so
|
||||
* caches invalidate correctly.
|
||||
*
|
||||
* Precedence: ETHERPAD_VERSION_STRING env var (explicit integrator
|
||||
* override) > sha256(version + "|" + gitVersion) > package.json version.
|
||||
*
|
||||
* For the original cache-busting rationale, see PR #3958.
|
||||
*/
|
||||
settings.randomVersionString = randomString(4);
|
||||
logger.info(`Random string used for versioning assets: ${settings.randomVersionString}`);
|
||||
const explicit = process.env.ETHERPAD_VERSION_STRING;
|
||||
if (explicit) {
|
||||
settings.randomVersionString = explicit;
|
||||
} else {
|
||||
const pkgVersion = require('../../package.json').version as string;
|
||||
settings.randomVersionString = createHash('sha256')
|
||||
.update(`${pkgVersion}|${settings.gitVersion || ''}`)
|
||||
.digest('hex')
|
||||
.slice(0, 8);
|
||||
}
|
||||
logger.info(`String used for versioning assets: ${settings.randomVersionString}`);
|
||||
};
|
||||
|
||||
export const exportedForTestingOnly = {
|
||||
|
||||
@ -147,4 +147,43 @@ describe(__filename, function () {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/ether/etherpad/issues/7213.
|
||||
// Pre-fix: randomVersionString was `randomString(4)`, regenerated on every
|
||||
// boot — the padbootstrap-<hash>.min.js filename therefore differed across
|
||||
// pods of the same build, producing 404s on any cross-pod request in a
|
||||
// horizontally-scaled deployment. Post-fix: the token is a deterministic
|
||||
// hash of version + gitVersion (or an explicit
|
||||
// ETHERPAD_VERSION_STRING env var).
|
||||
describe('randomVersionString determinism (issue #7213)', function () {
|
||||
it('is a stable 8-hex-char sha256 prefix by default', function () {
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
assert.match(settings.randomVersionString, /^[0-9a-f]{8}$/,
|
||||
`expected 8-char hex, got ${settings.randomVersionString}`);
|
||||
});
|
||||
|
||||
it('honours ETHERPAD_VERSION_STRING as an explicit override', function () {
|
||||
const settingsMod = require('../../../node/utils/Settings');
|
||||
const original = process.env.ETHERPAD_VERSION_STRING;
|
||||
const savedSettingsFile = settingsMod.settingsFilename;
|
||||
const savedCredsFile = settingsMod.credentialsFilename;
|
||||
const savedToken = settingsMod.randomVersionString;
|
||||
process.env.ETHERPAD_VERSION_STRING = 'integrator-1';
|
||||
settingsMod.settingsFilename = path.join(__dirname, 'settings.json');
|
||||
settingsMod.credentialsFilename = path.join(__dirname, 'credentials.json');
|
||||
try {
|
||||
// The token is set by reloadSettings, not by parseSettings alone.
|
||||
// Re-run the full reload path so the env var is consulted.
|
||||
settingsMod.reloadSettings();
|
||||
assert.strictEqual(settingsMod.randomVersionString, 'integrator-1',
|
||||
'ETHERPAD_VERSION_STRING should be used verbatim');
|
||||
} finally {
|
||||
if (original == null) delete process.env.ETHERPAD_VERSION_STRING;
|
||||
else process.env.ETHERPAD_VERSION_STRING = original;
|
||||
settingsMod.settingsFilename = savedSettingsFile;
|
||||
settingsMod.credentialsFilename = savedCredsFile;
|
||||
settingsMod.randomVersionString = savedToken;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user