Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
5 tasks done
oytuncoban opened this issue Jun 26, 2024 · 15 comments
Closed
5 tasks done

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

oytuncoban opened this issue Jun 26, 2024 · 15 comments

Comments

@oytuncoban
Copy link

oytuncoban commented Jun 26, 2024

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
Copy link
Member

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
Copy link

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
Copy link
Author

oytuncoban commented Jul 3, 2024

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
Copy link
Member

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
Copy link

danieloprado commented Jul 8, 2024

@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
Copy link

mes113 commented Jul 12, 2024

also interested in fix for this issue.

@ScriptedAlchemy
Copy link
Member

@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
Copy link
Member

2heal1 commented Jul 18, 2024

@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.

@mes113
Copy link

mes113 commented Jul 18, 2024

@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
Copy link
Contributor

YanPes commented Jul 24, 2024

Setting this a default strategy would be a nice DX boost

@AmazingJaze
Copy link

AmazingJaze commented Aug 1, 2024

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
Copy link

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
Copy link
Contributor

YanPes commented Oct 15, 2024

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
Copy link
Member

Amazing, thank you!

@YanPes
Copy link
Contributor

YanPes commented Oct 23, 2024

Docs updated, issue can be closed

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

No branches or pull requests

8 participants