Firefox fires an "error" event for a <link rel="modulepreload" crossorigin> whose target URL is already present in the module map
Categories
(Core :: DOM: Core & HTML, defect)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox155 | --- | fixed |
People
(Reporter: jrmuizel, Assigned: allstars.chh)
References
Details
(Keywords: webcompat:platform-bug)
User Story
user-impact-score:1600
Attachments
(1 file)
This was breaking https://bahn.de.
A test case is here: https://bug2052837.bmoattachments.org/attachment.cgi?id=9604828
| Reporter | ||
Comment 1•1 month ago
|
||
Jon, do you have a sense for how easy this would be to fix?
Updated•1 month ago
|
Updated•1 month ago
|
Comment 2•1 month ago
|
||
FWIW, I ran this through my / dom-core's bug triage skill:
AI-Generated Bug Triage
Note: This triage was generated automatically by an AI assistant.
Findings should be verified by an engineer before acting on them.
TL;DR
Valid spec-violation bug. When a <link rel="modulepreload"> targets a URL already present in the module map, Firefox fires an error event instead of load. Root cause: the module preload takes a fast path (WaitForModuleFetch) that skips network channel creation, so the preload is never registered with the PreloadService, causing PreloadLinkElement to interpret the missing preloader as failure. This breaks bahn.de login (S2/P1 webcompat). Fix is medium-sized — the preload service needs a way to know the module was already complete.
Triage: Bug 2052949 - Firefox fires an "error" event for a <link rel="modulepreload" crossorigin> whose target URL is already present in the module map
Classification
- Type: webcompat / feature-behavior
- Component: Core :: DOM: Core & HTML
Validity
Real bug. Spec is unambiguous. The fetch a single module script algorithm step 6 says: "If moduleMap[(url, moduleType)] exists, run onComplete given moduleMap[(url, moduleType)], and return." — the existing module script (non-null) should flow back through onComplete, which per the modulepreload processing model step 14.2 fires a load event. Chrome fires load; Firefox fires error. Test case attached to the bug reproduces reliably.
Severity Recommendation: S2
Blocks bahn.de login page (bug 2052837, S2/P1 webcompat). Vite/rolldown-built SPAs using __vitePreload helper will hit this pattern — a <script type=module> imports a module, then Vite appends a redundant <link rel=modulepreload crossorigin> for the same URL. Any site with a capture-phase error handler on resource elements will break.
Spec Situation
Spec is clear. fetch a single module script step 6: if the module map entry exists, run onComplete with the module script. The modulepreload link fires load when onComplete receives a non-null result (step 14.2). Firefox deviates by firing error.
Implementation Overview
The preload flow for <link rel=modulepreload>:
PreloadService::PreloadLinkElementcallsPreloadOrCoalesce, then checks if a preloader was registered. If not, callsNotifyNodeEvent(node, alreadyComplete).PreloadService::PreloadOrCoalescecallsPreloadScript→ScriptLoader::PreloadURI→StartLoad→StartModuleLoad.ModuleLoaderBase::StartOrRestartModuleLoadat line 589: if module is already in the module map, takes theWaitForModuleFetchfast path. This skipsStartFetch→StartLoadInternal, soNotifyOpenis never called and the preload is never registered with thePreloadService.- Back in
PreloadOrCoalesceline 266:LookupPreloadreturns null. - Returns
{nullptr, false}→NotifyNodeEventfireserror(becausefalse).
Contrast with a fresh modulepreload (module NOT in map): StartFetch → StartLoadInternal → channel opened → NotifyOpen registers the preload → LookupPreload finds it → preloader gets AddLinkPreloadNode → eventual load event. Works correctly.
Compare with stylesheets: PreloadOrCoalesce line 248 handles SheetPreloadStatus::AlreadyComplete by returning {nullptr, true} — which fires load. Module preloads have no equivalent path.
Size & Difficulty: Medium
Touches the boundary between the preload service and module loader. Several possible fix approaches, each touching 2-3 files. Needs a way to signal "module already complete" back from the module loader to the preload service. Regression risk is moderate — the preload/module interaction is complex and undertested for this edge case.
Confidence
| Dimension | Rating | Evidence |
|---|---|---|
| Reproducibility | Confirmed | Test case in bug reproduces, root cause traced through code |
| Fix direction | Probable | Code area identified, similar pattern exists for stylesheets (AlreadyComplete), but exact fix needs design consideration at the preload-service/module-loader boundary |
Where to Start
- The cleanest fix is likely modeled after the stylesheet
AlreadyCompletepattern.ScriptLoader::PreloadURI(or a new return value from it) needs to signal back toPreloadOrCoalescethat the module was already in the module map. ThenPreloadOrCoalescereturns{nullptr, true}→NotifyNodeEventfiresload. - Alternatively,
StartOrRestartModuleLoad'sWaitForModuleFetchpath could register the preload viaNotifyOpen(without a channel) when the request is a preload, so the normal completion flow firesloadvia the preloader. This is more invasive but might be more correct long-term. - Add a WPT test: import a module via
import(), then dynamically create a<link rel=modulepreload crossorigin>for the same URL, assert it firesloadnoterror.
Suggested Contacts
| Who | Role | Last Active | Why |
|---|---|---|---|
| Jon Coppeard (:jonco) | blame: WaitForModuleFetch author | active (bug 721236, 2026-07) | Wrote the module map waiting logic; reviewed modulepreload implementation |
| Yoshi Cheng-Hao Huang | blame: modulepreload pref/import-map code | active (bug 1916277, 2026-03) | Recent modulepreload-related changes |
| Olli Pettay (:smaug) | reviewer on modulepreload impl | active | Reviewed original modulepreload landing, DOM: Core & HTML peer |
Related Bugs
| Bug | Summary | Status | Relevance |
|---|---|---|---|
| 2052837 | bahn.de login page is blank | ASSIGNED (S2/P1) | Direct dependent — this bug is the platform root cause |
| 2042169 | bahn.de claims bot detected (Linux) | NEW (S4/P3) | Possibly same site, different issue |
| 1425310 | Implement modulepreload for link rel | FIXED | Original modulepreload implementation — the code that's now buggy |
| 1646776 | Fire load (not error) for preload links hitting CSS cache | FIXED | Exact same pattern for stylesheets — AlreadyComplete return value was the fix. Module preloads need equivalent handling. |
★ Insight ─────────────────────────────────────
Two preload tracking systems diverge. Firefox has two separate mechanisms tracking module loads: the module loader's own module map (mFetchingModules/mFetchedModules in ModuleLoaderBase) and the preload service's preload hash (PreloadService::RegisterPreload). The script preload registers with both only when it goes through the full network path (StartLoadInternal → NotifyOpen). The WaitForModuleFetch shortcut for already-fetched modules bypasses the network path entirely, so the preload service never learns the module exists — it sees a missing preloader and defaults to "error."
Bug 1646776 is the prior art. Stylesheets hit this exact same class of bug in 2020: a stylesheet already in cache wasn't registered as a preloader, so the <link rel=preload> fired error. The fix added a SheetPreloadStatus::AlreadyComplete return path. Module preloads need an analogous mechanism. Studying that fix (by Emilio) would be the fastest way to design this one.
─────────────────────────────────────────────────
| Assignee | ||
Updated•1 month ago
|
Updated•1 month ago
|
| Assignee | ||
Comment 3•1 month ago
|
||
When the preloading request finds an existing entry in module maps
(fetching/fetched), it returns silently and causes an error to be reported.
Adding two callbacks: OnNotifyPreloadOpen/OnNotifyPreloadStop to notify
the preload result.
test_bug_2052949.html to reproduce the reporting problem,
also add tests covering the fetching/fetched success, failure, and
syntax-error states in test_bug_2052949_states.html.
Updated•1 month ago
|
Updated•28 days ago
|
Updated•28 days ago
|
Updated•21 days ago
|
Updated•15 days ago
|
Created web-platform-tests PR https://github.com/web-platform-tests/wpt/pull/61647 for changes under testing/web-platform/tests
Upstream PR merged by moz-wptsync-bot
Description
•