Browse Source

fix: show offline-friendly error page when network is unavailable

When offline and navigating to an unvisited route, the ErrorBoundary
displayed a generic "Something went wrong" page instead of the friendly
offline/crunk-load error message. This happened because:

1. isChunkLoadError could miss errors when React wraps them or the error
   name doesn't exactly match — now also checks error.stack and adds a
   case-insensitive "chunkloaderror" pattern
2. When offline, ANY render error is overwhelmingly likely a chunk load
   failure — new isNetworkRelatedError() treats offline as network-related
3. The subtitle defaultMessage was wrong: it used isOffline instead of
   combining both networkRelated and isOffline dimensions
4. handleRetry only reloaded for isChunkLoadError — now also reloads when
   offline (resetting state can't re-fetch the chunk)
5. Auto-reloads the page when network comes back online during an error

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pull/11756/head
afc163 5 months ago
parent
commit
f2a950425b
  1. 62
      src/components/ErrorBoundary/index.tsx

62
src/components/ErrorBoundary/index.tsx

@ -2,17 +2,31 @@ import { getIntl } from '@umijs/max';
import { Button, Card, Result } from 'antd'; import { Button, Card, Result } from 'antd';
import React from 'react'; import React from 'react';
const CHUNK_ERROR_PATTERNS = [
/(?:loading|failed to load) (?:css )?chunk/i,
/imported module/i,
/chunkloaderror/i,
];
function isChunkLoadError(error: Error): boolean { function isChunkLoadError(error: Error): boolean {
return ( if (error.name === 'ChunkLoadError') return true;
error.name === 'ChunkLoadError' || // Check both message and stack trace (React may wrap errors)
/(?:loading|failed to load) (?:css )?chunk/i.test(error.message) || const text = `${error.message}\n${error.stack ?? ''}`;
/imported module/i.test(error.message) return CHUNK_ERROR_PATTERNS.some((p) => p.test(text));
); }
/** When offline, any render error is likely a chunk/network failure. */
function isNetworkRelatedError(error: Error, offline: boolean): boolean {
return offline || isChunkLoadError(error);
}
function getTitleId(networkRelated: boolean): string {
return networkRelated ? 'app.error.chunk.title' : 'app.error.render.title';
} }
function getSubTitleId(isChunkError: boolean, isOffline: boolean): string { function getSubTitleId(networkRelated: boolean, offline: boolean): string {
if (!isChunkError) return 'app.error.render.description'; if (!networkRelated) return 'app.error.render.description';
return isOffline return offline
? 'app.error.chunk.description.offline' ? 'app.error.chunk.description.offline'
: 'app.error.chunk.description.online'; : 'app.error.chunk.description.online';
} }
@ -47,7 +61,14 @@ export default class ErrorBoundary extends React.Component<
window.removeEventListener('offline', this.handleOffline); window.removeEventListener('offline', this.handleOffline);
} }
handleOnline = () => this.setState({ isOnline: true }); handleOnline = () => {
this.setState({ isOnline: true });
// Auto-reload when coming back online if the error was network-related
if (this.state.hasError && this.state.error) {
window.location.reload();
}
};
handleOffline = () => this.setState({ isOnline: false }); handleOffline = () => this.setState({ isOnline: false });
componentDidCatch(error: Error, info: React.ErrorInfo) { componentDidCatch(error: Error, info: React.ErrorInfo) {
@ -55,7 +76,11 @@ export default class ErrorBoundary extends React.Component<
} }
handleRetry = () => { handleRetry = () => {
if (this.state.error && isChunkLoadError(this.state.error)) { // For network-related errors, always reload (re-setting state won't re-fetch the chunk)
if (
this.state.error &&
isNetworkRelatedError(this.state.error, !this.state.isOnline)
) {
window.location.reload(); window.location.reload();
} else { } else {
this.setState({ hasError: false, error: null }); this.setState({ hasError: false, error: null });
@ -70,19 +95,22 @@ export default class ErrorBoundary extends React.Component<
const { error } = this.state; const { error } = this.state;
const intl = getIntl(); const intl = getIntl();
const isOffline = !this.state.isOnline; const isOffline = !this.state.isOnline;
const isChunkError = isChunkLoadError(error); const networkRelated = isNetworkRelatedError(error, isOffline);
const title = intl.formatMessage({ const title = intl.formatMessage({
id: isChunkError ? 'app.error.chunk.title' : 'app.error.render.title', id: getTitleId(networkRelated),
defaultMessage: isChunkError defaultMessage: networkRelated
? 'Failed to load page' ? 'Failed to load page'
: 'Something went wrong', : 'Something went wrong',
}); });
const subTitle = intl.formatMessage({ const subTitle = intl.formatMessage({
id: getSubTitleId(isChunkError, isOffline), id: getSubTitleId(networkRelated, isOffline),
defaultMessage: isOffline defaultMessage:
? 'Your network connection has been lost. Please check your connection and refresh.' networkRelated && isOffline
: 'Page resources failed to load. Please refresh and try again.', ? 'Your network connection has been lost. Please check your connection and refresh.'
: networkRelated
? 'Page resources failed to load. Please refresh and try again.'
: 'Sorry, an error occurred on this page. Please refresh or go back to the home page.',
}); });
return ( return (

Loading…
Cancel
Save