fix: prevent XBlock container crash when JS resources are blocked by browser extensions - #46
Conversation
ManuelStarDo
left a comment
There was a problem hiding this comment.
Hello @bra-i-am
Thank you for the work, again sorry for the delay in the review
Here are my findings:
Review: xblock init resilience fix (drag-and-drop-v2 cascading failure)
Overall: Solid, verifiable fix for the core reported bug. Tested with xblock-drag-and-drop-v2==5.0.2 (isolating from the separate 5.0.3 regression, see PR #287 review) by blocking the XBlock's JS/CSS at the network layer:
- ✅ Blocked block now shows a graceful "Loading..." placeholder instead of an infinite spinner.
- ✅ All sibling XBlocks in the same unit still render and are fully interactive — this is the actual bug being fixed, and it works.
- ✅ Console shows
console.warnfor the blocked resource instead of a silent failure.
This genuinely resolves the "one blocked resource kills the whole unit" issue for the case where the request is dropped.
⚠️ Gap: only covers "init function missing," not "init function throws"
In common/static/common/js/xblock/core.js, constructBlock only null-checks initFn before calling new Block():
if (!initFn) {
console.warn('XBlock init function not found:', $element.data('init'));
return null;
}
...
return new Block(); // not wrapped in try/catchIf initFn exists but throws during construction, the exception isn't caught here, and since initializeBlockLikes calls this via jQuery's .map() (no built-in isolation between iterations), the exception stops the loop — every XBlock after the failing one is left uninitialized.
I confirmed this is not hypothetical: with xblock-drag-and-drop-v2==5.0.3 (a real, published version), the vendor bundle throws during new Block() due to an unregistered virtualDom global under Studio's RequireJS setup. Result: Questão 3–6 (whose own JS/CSS load fine) never initialize — the exact cascading-failure symptom this PR sets out to fix, just triggered by a throw instead of a missing symbol. Stack trace confirms the throw propagates straight through jQuery.map with no intervening catch.
Note: cms/static/js/views/xblock.js's single-block AJAX path (handleXBlockFragment) already wraps XBlock.initializeBlock in try/catch — this same protection is just missing from the multi-block sweep.
Suggested fix:
try {
return new Block();
} catch (e) {
console.warn('XBlock init function threw during construction:', $element.data('init'), e);
return null;
}This lets the existing if (!block) { addClass('xblock-initialization-failed') } branch handle both failure modes.
Minor / not blocking
The container.js postMessage guard (if (data.type) { console.warn(...) }, fix #4 in the description) is low-risk and looks correct on inspection, but I didn't specifically reproduce the Redux DevTools type: undefined scenario it targets — flagging only for completeness.
Note
This fix is intentionally scoped to NAU's fork targeting the
teakrelease. It is not being submitted upstream because the issue no longer reproduces onmaster(Verawood), meaning the upstream codebase is already clean of this bug — likely resolved as a side effect of broader refactors between releases.Fixes https://github.com/fccn/nau-technical/issues/838
Fixes a resilience bug in Studio's XBlock container page where Chrome extensions (e.g. AdBlock) that block certain JS resources cause an infinite loading spinner and crash the entire unit — preventing all XBlocks from rendering, not just the one whose resource was blocked.
Root cause analysis:
When any JS resource fails to load,
addXBlockFragmentResourcescalleddeferred.reject()— and sincerenderXBlockFragmentonly hooks.done(), the XBlock HTML was never injected into the page. The server always sends the spinner as initial HTML; JavaScript is responsible for replacing it. With JS blocked, the spinner stayed forever.For the Drag & Drop v2 XBlock specifically, AdBlock filter lists target these two scripts by filename pattern:
drag_and_drop_v2/public/js/vendor/virtual-dom-*.jsdrag_and_drop_v2/public/js/drag_and_drop.*.jsBoth are served from the same domain (no CDN), so there is no cross-origin workaround available.
Four root causes fixed:
loadResourceusedViewUtils.loadJavaScript(backed by$script/scriptjs), which invokes its callback on bothonloadandonerror. When AdBlock blocks a script, the promise always resolved — making failures invisible to the application. Fixed by replacing it with a raw<script>element that wiresonerror → rejectdirectly.addXBlockFragmentResourcesrejected the deferred on first JS failure, which preventedrenderXBlockFragment's.done()handler from running — so the XBlock HTML was never injected and the spinner persisted. Fixed by resolving the deferred with a{failedJs: true}flag instead of rejecting, so the HTML is always injected regardless of whether the JS loaded. Awindow.failedXBlockResourcescache (mirroring the existingwindow.loadedXBlockResourcespattern) prevents redundant network requests for known-blocked resources on subsequent XBlock renders within the same page session.constructBlockincore.jscrashed withTypeErrorwhen an XBlock'sinitFnwas undefined (because its JS never loaded). The crash propagated to the outertry/catchinhandleXBlockFragment, which callederrorCallback()instead ofsuccessCallback()— leaving the container stuck. Fixed with a null check: ifinitFnis missing, the affected XBlock is marked withxblock-initialization-failedand a stub is returned, allowing the rest of the vertical to initialize normally. Additionally,fragmentsRendered.always()was changed to.done()so that XBlock initialization is only attempted when the fragment rendered successfully.container.jsmessage handler logged a spurious warning loop for everypostMessageevent withtype: undefinedsent by browser extension proxies (e.g. Redux DevTools) viasetInterval. Fixed by only logging for defined, unrecognized message types.Behavior after this fix:
xblock-initialization-failedCSS class. Their JS is not initialized, so interactive behavior is unavailable — but the block is visible and does not crash the page.console.warnis emitted for each blocked resource and each skipped init function, replacing the previous silent freeze.Before: Chrome + AdBlock → infinite spinner,
TypeError: Cannot read properties of undefined (reading 'prototype'), all XBlocks in the unit broken.After: Chrome + AdBlock → affected XBlock shows its server-rendered HTML with
xblock-initialization-failed, all other XBlocks render and initialize normally, no JS crash.Testing instructions
Prerequisites: A Studio unit with a Drag & Drop v2 XBlock alongside at least one other XBlock (e.g. Problem/Single Select).
We'll test this in
devenvironment usingnau-tutor-configs: run the environment with the commandbin/tutor dev startOpen Studio and navigate to a unit with a Drag & Drop v2 XBlock and some more XBlocks
Block the URLs manually through the dev tools of the browser, and you'll be able to replicate the bug like this
Change the
edx-platformbranch to this PR, enter the container by runningbin/tutor dev exec cms bashand compile the assets runningnpm run webpack-devin the containerFixed!