module-federation / core

Module Federation is a concept that allows developers to share code and resources across multiple JavaScript applications
https://module-federation.io/
MIT License
1.53k stars 235 forks source link

Can't create Error Fallback when any Remote fails. #2672

Closed oytuncoban closed 3 weeks ago

oytuncoban commented 4 months ago

Describe the bug

Hey, I was using the old Module Federation ('webpack/lib/container/ModuleFederationPlugin'), and decided to migrate to the new Module Federation 2.0.

However, in the previous MF version, I had to implement some ErrorBoundary components:

ErrorBoundary:

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo,
    });
    console.error('ErrorBoundary caught an error', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Something went wrong.</h1>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo?.componentStack}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

DynamicImport:

import React, { Suspense } from 'react';
import i18n from '../../helpers/i18n';
import queryClient from '../../queryClient';
import Loading from '../Loading';
import ErrorBoundary from './ErrorBoundary'; 

const sharedDeps = {
  i18n,
  queryClient,
};

const DynamicImport = ({ ImportRemoteApp, LoadingComponent = Loading }) => {
  return (
    <ErrorBoundary>
      <Suspense fallback={<LoadingComponent />}>
        <ImportRemoteApp {...sharedDeps} />
      </Suspense>
    </ErrorBoundary>
  );
};

export default DynamicImport;

Usage of DynamicImport:

import React from 'react';
import DynamicImport from '../components/DynamicImportComponent/DynamicImportComponent';

const #RemoteComponentName#> = React.lazy(() =>
  import('#RemoteModuleName#/#RemoteComponentName#').catch((error) => {
    console.error(`Failed to load module: ${'#RemoteModuleName#/#RemoteComponentName#'}`);
    console.error(error);
    return { default: <div>
      <h1>Something went wrong.</h1>
      <pre>{error?.message}</pre>
    </div> };
  })
);

const Component = () => {
  return <DynamicImport ImportRemoteApp={#RemoteComponentName#} />;
};

export default Component;

With this method, I was able to show Error UI when one of the remote is failing and has errors, or unavailable.

With Module Federation 2.0 I now get this error thrown by @module-federation/enhanced:

[ Federation Runtime ]: 
      Unable to use the **remote_name**'s 'http://localhost:3001/remoteEntry.js' URL with **remote_name**'s globalName to get remoteEntry exports.
      Possible reasons could be:

      1. 'http://localhost:3001/remoteEntry.js' is not the correct URL, or the remoteEntry resource or name is incorrect.

      2. muhakemat_davalar cannot be used to get remoteEntry exports in the window object.
    at error (http://localhost:3000/main.js:2176:11)
    at Object.assert (http://localhost:3000/main.js:2168:9)
    at http://localhost:3000/main.js:196:15

The versions of used packages: webpack: ^5.57.1, @module-federation/enhanced: ^0.2.1,

Example remotes object that I use:

{
remote_app1: "RemoteApp1Name@http://remote-url-1.domain.com/remoteEntry.js",
remote_app2: "RemoteApp2Name@http://remote-url-2.domain.com/remoteEntry.js"
}

Issue is that, webpack throws error and I cannot use my website as expected. Main goal is to keep Host(Shell) app to be continue working even if a remote fails. Users should be able to use other remotes and shell app layout.

I tried to implement the Dynamic System Host example. Here again, if I don't start one of the remotes, when user tries to load the failing remote, it immediately throws error and breaks the shell.

Wouldn't it be convenient that we can catch the Remote load errors and provide a fallback UI to the user?

Reproduction

https://github.com/module-federation/module-federation-examples/tree/master/dynamic-system-host

Used Package Manager

npm

System Info

System:
    OS: macOS 15.0
    CPU: (8) arm64 Apple M3
    Memory: 145.58 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 18.12.1 - ~/.nvm/versions/node/v18.12.1/bin/node
    Yarn: 4.0.2 - ~/.nvm/versions/node/v18.12.1/bin/yarn
    npm: 8.19.2 - ~/.nvm/versions/node/v18.12.1/bin/npm
    pnpm: 8.15.5 - ~/.nvm/versions/node/v18.12.1/bin/pnpm
  Browsers:
    Brave Browser: 108.1.46.144
    Chrome: 126.0.6478.116
    Edge: 126.0.2592.68
    Safari: 18.0

Validations

ScriptedAlchemy commented 4 months ago

You can use a runtime plugin to handle such cases, since the v1 way or catching errors was unreliable and did not work for import from ''

https://github.com/module-federation/module-federation-examples/blob/master/runtime-plugins/offline-remote/app1/offline-remote.js

https://module-federation.io/plugin/dev/index.html

danieloprado commented 4 months ago

The runtime plugin doesn't catch this kind of error, I'm stuck on version 0.1.6 until it get fixed 😢

Captura de ecrã 2024-07-01, às 15 55 50
oytuncoban commented 4 months ago

You can use a runtime plugin to handle such cases, since the v1 way or catching errors was unreliable and did not work for import from ''

https://github.com/module-federation/module-federation-examples/blob/master/runtime-plugins/offline-remote/app1/offline-remote.js

https://module-federation.io/plugin/dev/index.html

Couldn't try it yet due to the recent workload of my own. Gonna update as soon as I can.

ScriptedAlchemy commented 4 months ago

The runtime plugin doesn't catch this kind of error, I'm stuck on version 0.1.6 until it get fixed 😢

Captura de ecrã 2024-07-01, às 15 55 50

should be fixed now, this was a CORS error

danieloprado commented 4 months ago

@ScriptedAlchemy, the error persists, I created a repo example: https://github.com/danieloprado/mf-offline

When a shared is set, an uncaughtable error is thrown.

mes113 commented 4 months ago

also interested in fix for this issue.

ScriptedAlchemy commented 4 months ago

@2heal1 this seems like a legitimate issue in the runtime.

https://github.com/danieloprado/mf-offline

Any ideas on how we can patch it so it doesn't fail

2heal1 commented 3 months ago

@2heal1 this seems like a legitimate issue in the runtime.

https://github.com/danieloprado/mf-offline

Any ideas on how we can patch it so it doesn't fail

The error happens because the default shared strategy is version first which means it will fetch remote entry first and then sync the sharedScope . But in this case , the remote is offline , so the request failed .

It can be solved by change the default shared strategy without any source code change:

  1. add shared-strategy.ts file in the project root
    
    import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';

const sharedStrategy: () => FederationRuntimePlugin = () => ({ name: 'shared-strategy-plugin', beforeInit(args) { const { userOptions } = args; const shared = userOptions.shared; if (shared) { Object.keys(shared).forEach((sharedKey) => { const sharedConfigs = shared[sharedKey]; const arraySharedConfigs = Array.isArray(sharedConfigs) ? sharedConfigs : [sharedConfigs]; arraySharedConfigs.forEach((s) => { s.strategy = 'loaded-first'; }); }); } return args; }, }); export default sharedStrategy;

2. apply the runtime plugin

```diff
// rsbuild.config.ts
import path from 'path';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
  server: { port: 3000 },
  dev: { assetPrefix: 'http://localhost:3000' },
  plugins: [pluginReact()],
  html: {
    title: 'MFE Offline Test'
  },
  tools: {
    rspack: {
      plugins: [
        new ModuleFederationPlugin({
          name: 'test_host',
          remotes: {
            'app_offline': 'app_offline@http://localhost:3001/manifest.json'
          },
+          runtimePlugins:[path.resolve(__dirname,'shared-strategy.ts')],
          shared: ['react'] // comment and it will work
        })
      ]
    }
  }
});

And to prevent this issue , i think we should add sharedStrategy config in build plugin , and also make it as default strategy.

mes113 commented 3 months ago

@2heal1 this seems like a legitimate issue in the runtime. https://github.com/danieloprado/mf-offline Any ideas on how we can patch it so it doesn't fail

The error happens because the default shared strategy is version first which means it will fetch remote entry first and then sync the sharedScope . But in this case , the remote is offline , so the request failed .

It can be solved by change the default shared strategy without any source code change:

  1. add shared-strategy.ts file in the project root
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';

const sharedStrategy: () => FederationRuntimePlugin = () => ({
  name: 'shared-strategy-plugin',
  beforeInit(args) {
    const { userOptions } = args;
    const shared = userOptions.shared;
    if (shared) {
      Object.keys(shared).forEach((sharedKey) => {
        const sharedConfigs = shared[sharedKey];
        const arraySharedConfigs = Array.isArray(sharedConfigs)
          ? sharedConfigs
          : [sharedConfigs];
        arraySharedConfigs.forEach((s) => {
          s.strategy = 'loaded-first';
        });
      });
    }
    return args;
  },
});
export default sharedStrategy;
  1. apply the runtime plugin
// rsbuild.config.ts
import path from 'path';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
  server: { port: 3000 },
  dev: { assetPrefix: 'http://localhost:3000' },
  plugins: [pluginReact()],
  html: {
    title: 'MFE Offline Test'
  },
  tools: {
    rspack: {
      plugins: [
        new ModuleFederationPlugin({
          name: 'test_host',
          remotes: {
            'app_offline': 'app_offline@http://localhost:3001/manifest.json'
          },
+          runtimePlugins:[path.resolve(__dirname,'shared-strategy.ts')],
          shared: ['react'] // comment and it will work
        })
      ]
    }
  }
});

And to prevent this issue , i think we should add sharedStrategy config in build plugin , and also make it as default strategy.

yes with this one issue solved .

And to prevent this issue , i think we should add sharedStrategy config in build plugin , and also make it as default strategy.

i personally think that its a good idea .

YanPes commented 3 months ago

Setting this a default strategy would be a nice DX boost

AmazingJaze commented 3 months ago

Glad to stumble upon this solution as we were just noticing similar problems with our own Error Fallbacks not working.

Question for @2heal1 about the implications of this strategy: does loaded-first mean that the shared package versions requested by the top level host are guaranteed to be the chosen versions because the host is always going to be loaded first?

Or is the loaded-first resolution strategy when dealing with shared packages requested by the host less deterministic than even that?

VugarAhmadov commented 2 months ago

For those who are using rspack v1, and getting this warning on the console [ Federation Runtime ]: "shared.strategy is deprecated, please set in initOptions.shareStrategy instead!"

To solve this, you can just use this code

import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';

const sharedStrategy: () => FederationRuntimePlugin = () => ({
  name: 'shared-strategy-plugin',
  beforeInit(args) {
    args.userOptions.shareStrategy = 'loaded-first';
    return args;
  },
});

export default sharedStrategy;
YanPes commented 1 month ago

As the shared-strategy is mentioned as solution in several Issues, I will move the solution from @2heal1 to the Error catalog section of the docs for more transpareny

ScriptedAlchemy commented 3 weeks ago

Amazing, thank you!

YanPes commented 3 weeks ago

Docs updated, issue can be closed