We shipped a release that renamed a JavaScript global. The PHP that writes the global was updated. The JS that reads it was updated. Both files were on the server. The feature did nothing.
Page source showed the new global being written. The bundle being loaded still contained the old reader. New writer, old reader, no error, no console warning, no clue.
This is a specific trap, but the shape is general enough to be worth knowing whatever framework you are in: an asset cache-buster only busts the cache it addresses. If there is a second cache layer between you and the browser, and it is keyed on something you did not change, your version parameter is decorative.
Layer one: the browser cache
Register a front-end asset with no version and you get a plain tag with no query string:
<script src="modules/mymodule/views/js/upload.js"></script>
Static assets on most hosts are served with far-future Cache-Control. So after you fix a JS bug, sync files and clear the framework cache, returning visitors keep running the old file until they hard-refresh. New visitors get the fix. That asymmetry is why a "still broken" report can be entirely browser cache, and why it is worth confirming before you debug anything:
fetch(url, { cache: 'no-store' }).then(r => r.text()).then(t => console.log(t.slice(0, 200)));
If that shows the new file, the deployed file is fine and you are looking at a cache problem, not a code problem.
The fix in PrestaShop is a dedicated parameter, not a hand-built query string:
$controller->registerJavascript(
'modules-mymodule-upload',
'modules/' . $this->name . '/views/js/upload.js',
['position' => 'bottom', 'priority' => 500, 'version' => $this->version]
);
JavascriptManager::add() appends ?<version> after it has resolved the path on disk.
The trap inside the fix
Do not put the query string in the path yourself:
// broken
$controller->registerJavascript($id, 'modules/x/views/js/upload.js?v=2.1.0', [...]);
AbstractAssetManager::getFullPath() runs file_exists() on the raw relative path before any version handling. upload.js?v=2.1.0 does not exist as a file, so the asset is never registered at all — silently. No tag, no error.
A related caution from a live test: Media::getMediaPath(), used by the legacy addJS()/addCSS() path, does strip the query before its own file_exists(), so reading the core source suggests a manual ?v= is safe there. On an actual PrestaShop 9.1.4 shop it was not: adding ?v=<version> to addJS() inside a hookDisplayHeader() made the browser stop requesting the file entirely — zero requests in the Network tab, while other output from the same hook rendered fine. We reverted it and never fully isolated the cause. The lesson is not about that one API; it is that a correct-looking static read of framework source is not a substitute for a live test on the target version.
Layer two: the combined bundle, which the version does not touch
With version in place we shipped the rename. It still did not work.
PrestaShop's CCC ("combine, compress, cache") option concatenates all the page's JS into one bundle. CccReducer::reduceJs() names that bundle:
<position>-<sha1(joined file paths)><PS_CCCJS_VERSION>.js
Read that carefully. The hash is over the file paths, not the file contents. And JavascriptManager::add() stores the version-less $srcPath as the asset's path.
So:
- changing a file's contents does not change the bundle name;
- changing the
versionparameter does not change the bundle name either; - the reducer checks
filesystem->exists()on that name, finds the old bundle, and serves it.
Forever. There is no expiry.
Observed live: the page HTML emitted the new csfNumOfAttachments global from the new PHP, while bottom-3fc480223.js still contained the previous contact-upload.js reading window.numofattachments. Both files on disk were correct. The bundle in between was a fossil.
The fix that actually covers both layers
When a release changes the content of a front-end JS or CSS file, the upgrade script has to invalidate the bundle explicitly:
function upgrade_module_1_3_0($module)
{
// Clears themes/<theme>/assets/cache/* and bumps
// PS_CCCJS_VERSION / PS_CCCCSS_VERSION.
Media::clearCache();
return true;
}
You need both:
-
'version'handles the non-combined case (CCC off, or assets served individually). -
Media::clearCache()handles the combined case.
Neither one covers the other.
Why this class of bug is expensive
It does not look like a caching problem. It looks like your feature is broken. The writer and the reader are both correct in the repository, both correct on disk, and disagree only in the artefact the framework assembled between them. Every instinct says "read the code again", and the code is fine.
Two habits that shorten it:
-
When a fix does not take effect, verify what is actually being served before re-reading your source. Fetch the deployed asset with
cache: 'no-store'. Look at the bundle, not the file. - Treat "rename a shared symbol" as a deployment problem, not just a code change. Any rename that splits a writer from its reader across two files will fail this way if either side is stale. If you can, ship the change so the reader tolerates both names for one release. Then the fossil bundle is merely old, not broken.
Neither is exotic. Both are things we now do because the alternative cost us a release.
From maintaining around sixty PrestaShop modules at MEG Venture. The specifics above are PrestaShop 1.7 through 9, but any framework with an asset pipeline that concatenates by path has the same hole.
Top comments (0)