# Building a Custom Retry Plugin This guide shows how to create a custom retry plugin with advanced features like circuit breaker patterns, fallback components, and intelligent error handling for Module Federation applications. ## Overview When building distributed applications with Module Federation, handling offline remotes gracefully is crucial for maintaining application stability. A custom retry plugin can provide sophisticated error handling beyond basic retry mechanisms. ## Core Features A robust retry plugin should include: - **Retry Logic**: Configurable retry attempts with exponential backoff - **Circuit Breaker Pattern**: Prevent cascading failures by temporarily disabling failed remotes - **Fallback Components**: Graceful UI degradation when remotes are unavailable - **Timeout Handling**: Prevent hanging requests - **Intelligent State Management**: Track remote health and failure patterns ## Implementation ### Basic Plugin Structure ```ts import type { ModuleFederationRuntimePlugin } from "@module-federation/enhanced/runtime"; interface RetryConfig { enableLogging?: boolean; fallbackTimeout?: number; retryAttempts?: number; retryDelay?: number; enableCircuitBreaker?: boolean; circuitBreakerThreshold?: number; circuitBreakerResetTimeout?: number; } const customRetryPlugin = (config: RetryConfig = {}): ModuleFederationRuntimePlugin => { const { enableLogging = true, fallbackTimeout = 5000, retryAttempts = 2, retryDelay = 1000, enableCircuitBreaker = true, circuitBreakerThreshold = 3, circuitBreakerResetTimeout = 60000, } = config; return { name: "custom-retry-plugin", // Implementation details below... }; }; ``` ### State Management Track remote health using a state management system: ```ts interface RemoteState { failureCount: number; lastFailureTime: number; isCircuitOpen: boolean; } // Track remote states for circuit breaker pattern const remoteStates = new Map<string, RemoteState>(); const getRemoteState = (remoteId: string): RemoteState => { if (!remoteStates.has(remoteId)) { remoteStates.set(remoteId, { failureCount: 0, lastFailureTime: 0, isCircuitOpen: false, }); } return remoteStates.get(remoteId)!; }; const updateRemoteState = (remoteId: string, isSuccess: boolean) => { const state = getRemoteState(remoteId); if (isSuccess) { // Reset on success state.failureCount = 0; state.isCircuitOpen = false; } else { // Increment failure count state.failureCount++; state.lastFailureTime = Date.now(); // Open circuit if threshold reached if (enableCircuitBreaker && state.failureCount >= circuitBreakerThreshold) { state.isCircuitOpen = true; // Auto-reset circuit after timeout setTimeout(() => { state.isCircuitOpen = false; state.failureCount = 0; }, circuitBreakerResetTimeout); } } }; ``` ### Retry Logic with Timeout Implement exponential backoff with timeout protection: ```ts const withRetry = async <T>( operation: () => Promise<T>, remoteId: string, attempts = retryAttempts ): Promise<T> => { let lastError: Error; for (let i = 0; i < attempts; i++) { try { const result = await Promise.race([ operation(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Request timeout')), fallbackTimeout) ) ]); // Success - update state updateRemoteState(remoteId, true); return result; } catch (error) { lastError = error as Error; // Wait before retry (exponential backoff) if (i < attempts - 1) { await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1))); } } } // All attempts failed updateRemoteState(remoteId, false); throw lastError!; }; ``` ### Error Handling by Lifecycle Handle different error types based on the failure stage: ```ts async errorLoadRemote(args) { const { id, error, from, lifecycle } = args; const remoteId = id || from || 'unknown'; // Different handling based on lifecycle switch (lifecycle) { case 'beforeRequest': case 'afterResolve': // Manifest loading failed const state = getRemoteState(remoteId); if (state.isCircuitOpen) { return createFallbackModule(remoteId, error); } try { // Retry the original request return await withRetry(() => args.origin(), remoteId); } catch (retryError) { return createFallbackModule(remoteId, error); } case 'onLoad': // Module loading failed after manifest was loaded return () => createFallbackModule(remoteId, error); case 'beforeLoadShare': // Shared dependency loading failed return createFallbackModule(remoteId, error); default: return createFallbackModule(remoteId, error); } } ``` ### Fallback Components Create graceful fallback UI components: ```ts const createFallbackComponent = (remoteId: string, error?: Error) => { const FallbackComponent = async () => { // Dynamically import React to avoid eager loading issues const React = await import('react'); // Build error details safely const errorDetails = error ? [ React.createElement("details", { key: "error" }, [ React.createElement("summary", { key: "summary" }, "Error Details"), React.createElement("pre", { key: "error-details", style: { overflow: "auto", fontSize: "12px", textAlign: "left" as const } }, error?.message || String(error)) ]) ] : null; return React.createElement("div", { style: { padding: "16px", margin: "8px", border: "2px dashed #ffa39e", borderRadius: "8px", backgroundColor: "#fff2f0", color: "#cf1322", textAlign: "center" as const, }, }, [ React.createElement("h3", { key: "title" }, "Remote Module Unavailable"), React.createElement("p", { key: "description" }, `The remote module "${remoteId}" is currently offline.`), errorDetails ].filter(Boolean)); }; FallbackComponent.displayName = `FallbackComponent_${remoteId}`; return FallbackComponent; }; const createFallbackModule = (remoteId: string, error?: Error) => { try { const FallbackComponent = createFallbackComponent(remoteId, error); return { __esModule: true, default: FallbackComponent, [remoteId]: FallbackComponent, }; } catch (createError) { // If fallback creation fails, return minimal module console.error('Failed to create fallback module:', createError); return { __esModule: true, default: () => null, }; } }; ``` ## Usage ### Configuration ```ts import { init } from '@module-federation/enhanced/runtime'; import customRetryPlugin from './custom-retry-plugin'; init({ name: 'host', remotes: [ { name: 'remote1', entry: 'http://localhost:3001/remoteEntry.js' } ], plugins: [ customRetryPlugin({ enableLogging: true, retryAttempts: 3, retryDelay: 1000, fallbackTimeout: 5000, enableCircuitBreaker: true, circuitBreakerThreshold: 5, circuitBreakerResetTimeout: 30000, }) ] }); ``` ### Integration with Share Strategy Be aware of how `shareStrategy` affects remote loading: ```ts // Version-first strategy eagerly loads remotes // This can trigger failures during app startup shareStrategy: 'version-first' // Loaded-first strategy loads remotes on-demand // Failures occur only when modules are actually requested shareStrategy: 'loaded-first' // Recommended for offline scenarios ``` For applications that need to handle offline scenarios gracefully, consider using `loaded-first` strategy combined with your retry plugin. ## Best Practices 1. **Graceful Degradation**: Always provide meaningful fallback UI 2. **Circuit Breaker**: Prevent cascading failures with circuit breaker pattern 3. **Logging**: Include comprehensive logging for debugging 4. **Performance**: Cache fallback modules to avoid recreation 5. **Recovery**: Allow automatic recovery when remotes come back online 6. **Configuration**: Make retry behavior configurable per environment ## Complete Example Here's a simplified version of a production-ready retry plugin: ```ts import type { ModuleFederationRuntimePlugin } from "@module-federation/enhanced/runtime"; import type { ComponentType } from "react"; interface OfflineFallbackConfig { enableLogging?: boolean; fallbackTimeout?: number; retryAttempts?: number; retryDelay?: number; enableCircuitBreaker?: boolean; circuitBreakerThreshold?: number; circuitBreakerResetTimeout?: number; fallbackComponents?: Record<string, ComponentType>; } const enhancedOfflineFallbackPlugin = ( config: OfflineFallbackConfig = {} ): ModuleFederationRuntimePlugin => { const { enableLogging = true, fallbackTimeout = 5000, retryAttempts = 2, retryDelay = 1000, enableCircuitBreaker = true, circuitBreakerThreshold = 3, circuitBreakerResetTimeout = 60000, fallbackComponents = {}, } = config; const remoteStates = new Map<string, any>(); const fallbackCache = new Map<string, ComponentType>(); const log = (message: string, ...args: any[]) => { if (enableLogging) { console.warn(`[OfflineFallbackPlugin] ${message}`, ...args); } }; const createFallbackComponent = (remoteId: string, error?: Error) => { if (fallbackComponents[remoteId]) { return fallbackComponents[remoteId]; } const FallbackComponent = async () => { // Dynamically import React to avoid eager loading issues const React = await import('react'); return React.createElement("div", { style: { padding: "16px", border: "2px dashed #ffa39e", borderRadius: "8px", backgroundColor: "#fff2f0", color: "#cf1322", textAlign: "center", }, }, [ React.createElement("h3", { key: "title" }, "Remote Module Unavailable"), React.createElement("p", { key: "description" }, `The remote module "${remoteId}" is currently offline.`), ]); }; return FallbackComponent; }; return { name: "enhanced-offline-fallback-plugin", async errorLoadRemote(args) { const { id, error, lifecycle } = args; log(`Remote loading failed: ${id}`, { lifecycle, error: error?.message }); switch (lifecycle) { case 'afterResolve': // Manifest loading failed return { id: 'fallback', name: 'fallback', metaData: {}, shared: [], remotes: [], exposes: [] }; case 'onLoad': // Module loading failed return () => ({ __esModule: true, default: createFallbackComponent(id, error), }); default: return createFallbackComponent(id, error); } }, onLoad(args) { log(`Successfully loaded remote: ${args.id}`); return args; }, }; }; export default enhancedOfflineFallbackPlugin; ``` ### Usage in Rspack Configuration ```ts import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack"; import enhancedOfflineFallbackPlugin from "./enhanced-offline-fallback-plugin"; export default defineConfig({ plugins: [ new ModuleFederationPlugin({ name: "hostApp", shareStrategy: "loaded-first", // Recommended for offline handling remotes: { "remote-app": "remoteApp@http://localhost:8081/remote-mf-manifest.json", }, runtimePlugins: [ "./enhanced-offline-fallback-plugin.ts", ], }), ], }); ``` ## Advanced Features Consider adding these advanced features: - **Health Checks**: Periodic health checks for circuit breaker recovery - **Alternative Sources**: Try loading from multiple manifest URLs - **Metrics**: Collect metrics on remote reliability - **User Notifications**: Notify users about offline remotes - **Progressive Enhancement**: Gracefully handle partial functionality This approach provides robust error handling while maintaining a good user experience even when remote modules are unavailable.