🐛 Fixed race condition causing 0-byte cards.min.js and cards.min.css#26515
🐛 Fixed race condition causing 0-byte cards.min.js and cards.min.css#26515betschki wants to merge 2 commits intoTryGhost:mainfrom
Conversation
When concurrent requests hit serveMiddleware() before assets are ready, each independently called load() which triggers fs.writeFile. Because writeFile uses O_TRUNC, concurrent writes truncate the file to 0 bytes before writing — any concurrent read between truncate and write sees an empty file. The static file middleware then caches 0 bytes in memory with a 1-year Cache-Control header, permanently serving empty assets until restart. Applied the Singleton Promise pattern: store the in-flight load() promise so concurrent callers await the same one. A promise-identity guard ensures that invalidate() called mid-flight does not allow a stale .finally() to clobber a newer loading promise.
WalkthroughThis pull request modifies the AssetsMinificationBase class to implement single-flight concurrency control for asset loading operations. A new 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ghost/core/test/unit/frontend/services/assets-minification/assets-minification-base.test.js (1)
196-216: Consider assertingnextis not called whenload()throws.The test confirms
assets.loadingis cleared on error, but doesn't assert thatnextis never invoked when the middleware rejects. Addingassert.equal(next.callCount, 0)afterassert.rejects(...)would make the contract fully explicit and guard against a future refactor accidentally callingnext()on error.✅ Proposed addition
await assert.rejects(() => middleware({}, {}, next)); + assert.equal(next.callCount, 0, 'next() must not be called when load() throws'); assert.equal(assets.loading, null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/test/unit/frontend/services/assets-minification/assets-minification-base.test.js` around lines 196 - 216, The test should also assert the middleware does not call the next handler when load() throws; after the existing await assert.rejects(() => middleware({}, {}, next)); add an assertion that next.callCount (or next.called) is 0 to ensure the serveMiddleware() path for the TestAssets class does not invoke next on error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@ghost/core/test/unit/frontend/services/assets-minification/assets-minification-base.test.js`:
- Around line 196-216: The test should also assert the middleware does not call
the next handler when load() throws; after the existing await assert.rejects(()
=> middleware({}, {}, next)); add an assertion that next.callCount (or
next.called) is 0 to ensure the serveMiddleware() path for the TestAssets class
does not invoke next on error.
When concurrent requests hit
serveMiddleware()before assets are ready, each independently callsload()which triggersfs.writeFile. BecausewriteFileusesO_TRUNC, concurrent writes truncate the file to 0 bytes before writing − any concurrentreadFilebetween truncate and write sees an empty file. ThecreatePublicFileMiddlewareinserve-public-file.jsthen caches this 0-byte response in memory with a 1-yearCache-Controlheader, permanently serving emptycards.min.js/cards.min.cssuntil restart.Impact: Card-related JavaScript and CSS (audio players, galleries, bookmarks, etc.) silently break − the browser receives a 200 OK with an empty body. Audio player timestamps show raw seconds instead of formatted time, gallery layouts break, bookmark cards don't render, etc.
Node.js
fs.writeFiledocs explicitly warn:The proposed fix uses a singleton promise pattern to deduplicate concurrent
load()calls, ensuring the asset file is only written once regardless of how many requests arrive before it's ready.Why is this needed?
On Magic Pages this issue was observed multiple times in containerized deployments with zero-downtime rolling updates (Docker Swarm
start-first, Kubernetes rolling updates), where traffic is routed to a fresh container before assets have been built. Health checks or multiple users hitting the site simultaneously can trigger concurrent first-requests. This isn't an issue on smaller sites, but has been observed with bigger sites.Got some code for us? Awesome 🎊!
Please take a minute to explain the change you're making:
Please check your PR against these items:
We appreciate your contribution! 🙏