Skip to content

fix: defer shared bridging to getExposes to prevent init deadlock - #1325

Merged
gioboa merged 4 commits into
module-federation:mainfrom
RalphK66:fix/defer-bridge-to-prevent-init-deadlock
Sep 19, 2026
Merged

gioboa merged 4 commits into
module-federation:mainfrom
RalphK66:fix/defer-bridge-to-prevent-init-deadlock

Conversation

@RalphK66

@RalphK66 RalphK66 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

closes #1326

Summary

Fixes a deadlock where a webpack host loading Vite remotes that share the same React version hangs indefinitely — import('./bootstrap') never resolves.

Deadlock chain

  1. Webpack host's import('./bootstrap') → sharing-consume chunk → loadShare("react")initializeSharing → awaits all remote init promises
  2. Vite remote's init() eagerly runs await __mfBridgeSharedProviders()__mfBridgeExternalSharedProviderloadShare(pkg) back on the host
  3. Host's loadShareinitializeSharing → same cached promise → awaits the Vite remote's still-pending init()circular deadlock

Fix

The __mfLateBridgeShared deferral mechanism already exists and runs in getExposes(). This change assigns it unconditionally instead of eagerly awaiting __mfBridgeSharedProviders() during init(). Both the materialized-batch bridging and the global-instances bridging are wrapped into a single deferred function. By the time getExposes() runs, all remote inits have completed and initializeSharing resolves without circular waits.

Before (deadlocks):

await __mfBridgeSharedProviders();
if (__mfUsesWebpackShareScope) {
  __mfLateBridgeShared = __mfBridgeSharedProviders;
}
// ... global instances bridging runs eagerly too ...

After (defers):

__mfLateBridgeShared = async () => {
  await __mfBridgeSharedProviders();
  await __mfBridgeGlobalSharedProviders();
};

Reproduction

Minimal repro: https://github.com/RalphK66/mf-vite-init-deadlock-repro

1 webpack host + 2 Vite remotes, all sharing React 19 at the same version. The host page deadlocks on every load — import('./bootstrap') never resolves.

Note on runtime-core

loadShare() in @module-federation/runtime-core unconditionally awaits initializeSharing() which includes ALL remote init promises. Any remote that calls loadShare during its init() can trigger this cycle. This PR breaks the cycle from the Vite side; the core runtime could independently be made more resilient.

Test plan

  • All 1398 existing tests pass (one test updated to reflect the unconditional deferral)
  • Verified fix against minimal repro (1 webpack host + 2 Vite remotes)
  • Verified fix against larger topology (webpack host + 6 Vite remotes + 6 Vite relays with mixed React versions)

When a webpack host loads a Vite remote that shares the same React
version, a circular deadlock can occur: the host's loadShare →
initializeSharing awaits the Vite remote's init, but the remote's
init eagerly runs __mfBridgeSharedProviders which calls loadShare
back on the host, whose initializeSharing awaits the remote's
still-pending init — a cycle that never resolves.

The __mfLateBridgeShared deferral mechanism already exists and runs
in getExposes(), but was only assigned conditionally when
__mfUsesWebpackShareScope was true. This change assigns it
unconditionally, deferring both __mfBridgeSharedProviders and the
global instances bridging to getExposes() where all remote inits
have already completed.
@pkg-pr-new

pkg-pr-new Bot commented Sep 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@module-federation/vite@1325

commit: 83a1983

@gioboa gioboa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip the eager bridge in init() for webpack hosts; their loadShare() awaits this init, so bridging re-entered it and hung. Regression from #1115. Verified on the #1326 and #1064 repros, unit 1398, e2e 29/29, integration 28/28.

@RalphK66 thanks for the precise diagnosis and the repro, they made this easy to pin down. We're landing a narrower version of your fix: same idea, but the deferral only applies when the host's share scope is webpack's, so Vite hosts keep bridging inside init() as before.

@gioboa gioboa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @RalphK66

Since module-federation#1275 a subpath share the host does not provide is seeded locally
during init(), which evaluates the package's dev proxy before the late
bridge runs. Dev proxies snapshot their exports, so the remote kept its
own React copy under webpack/rspack hosts (hooks crash). Prod proxies
rebind and are unchanged.
Guards the remote init() deadlock (module-federation#1326, module-federation#1064) and single-React
rendering with same-version shared React.
@gioboa

gioboa commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

@RalphK66 two more commits on top. While building an e2e for this I found that deferring the bridge alone leaves the remote on its own React copy with a Vite 6/7 dev remote (add a useState to your widget to see it), because a subpath share seeded during init() evaluates the react proxy before the bridge runs. The first commit skips that seeding in dev when the provider is still pending; the second adds a webpack + rspack host fixture so both the deadlock and single-React rendering stay covered.

@gioboa
gioboa merged commit 24f95ff into module-federation:main Sep 19, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Vite remote init() deadlocks webpack host when bridging shared modules of the same version

3 participants