fix(core): handle the getSession network error properly (#4909)

If network offline or API error happens, the `session` returned by the `useSession` hook will be null, so we can't assume it is not null.

There should be following changes:
1. create a page in ErrorBoundary to let the user refetch the session.
2. The `SessionProvider` stop to pull the new session once the session is null, we need to figure out a way to pull the new session when the network is back or the user click the refetch button.
This commit is contained in:
LongYinan
2023-11-17 16:50:48 +08:00
committed by GitHub
parent 87248b3337
commit 62c0efcfd1
14 changed files with 385 additions and 157 deletions
@@ -0,0 +1,41 @@
import { style } from '@vanilla-extract/css';
export const errorLayout = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
});
export const errorDetailStyle = style({
display: 'flex',
flexDirection: 'column',
maxWidth: '420px',
});
export const errorTitle = style({
fontSize: '36px',
lineHeight: '44px',
fontWeight: 700,
});
export const errorImage = style({
height: '178px',
maxWidth: '400px',
flexGrow: 1,
});
export const errorDescription = style({
marginTop: '24px',
});
export const errorRetryButton = style({
marginTop: '24px',
width: '94px',
});
export const errorDivider = style({
width: '20px',
height: '100%',
});
@@ -0,0 +1,176 @@
import type {
QueryParamError,
Unreachable,
WorkspaceNotFoundError,
} from '@affine/env/constant';
import { PageNotFoundError } from '@affine/env/constant';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { Button } from '@toeverything/components/button';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
getCurrentStore,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
import type React from 'react';
import { Component, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import {
RecoverableError,
SessionFetchErrorRightAfterLoginOrSignUp,
} from '../../unexpected-application-state/errors';
import {
errorDescription,
errorDetailStyle,
errorDivider,
errorImage,
errorLayout,
errorRetryButton,
errorTitle,
} from './affine-error-boundary.css';
import errorBackground from './error-status.assets.svg';
export type AffineErrorBoundaryProps = React.PropsWithChildren;
type AffineError =
| QueryParamError
| Unreachable
| WorkspaceNotFoundError
| PageNotFoundError
| Error
| SessionFetchErrorRightAfterLoginOrSignUp;
interface AffineErrorBoundaryState {
error: AffineError | null;
canRetryRecoveredError: boolean;
}
export const DumpInfo = () => {
const location = useLocation();
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname;
const query = useParams();
useEffect(() => {
console.info('DumpInfo', {
path,
query,
currentWorkspaceId,
currentPageId,
metadata,
});
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
return null;
};
export class AffineErrorBoundary extends Component<
AffineErrorBoundaryProps,
AffineErrorBoundaryState
> {
override state: AffineErrorBoundaryState = {
error: null,
canRetryRecoveredError: true,
};
private readonly handleRecoverableRetry = () => {
if (this.state.error instanceof RecoverableError) {
if (this.state.error.canRetry()) {
this.state.error.retry();
this.setState({
error: this.state.error,
canRetryRecoveredError: this.state.error.canRetry(),
});
} else {
document.location.reload();
}
}
};
static getDerivedStateFromError(
error: AffineError
): AffineErrorBoundaryState {
return {
error,
canRetryRecoveredError:
error instanceof RecoverableError ? error.canRetry() : true,
};
}
override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
override render(): ReactNode {
if (this.state.error) {
let errorDetail: ReactElement | null = null;
const error = this.state.error;
if (error instanceof PageNotFoundError) {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
<>
<span> Page error </span>
<span>
Cannot find page {error.pageId} in workspace{' '}
{error.workspace.id}
</span>
</>
</>
);
} else if (error instanceof SessionFetchErrorRightAfterLoginOrSignUp) {
const retryButtonDesc = this.state.canRetryRecoveredError
? 'Refetch'
: 'Reload';
errorDetail = (
<>
<h1 className={errorTitle}>Sorry.. there was an error</h1>
<span className={errorDescription}> Fetching session failed </span>
<span className={errorDescription}>
If you are still experiencing this issue, please{' '}
<a
style={{ color: 'var(--affine-primary-color)' }}
href="https://community.affine.pro"
target="__blank"
>
contact us through the community.
</a>
</span>
<Button
className={errorRetryButton}
onClick={this.handleRecoverableRetry}
type="primary"
>
{retryButtonDesc}
</Button>
</>
);
} else {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
{error.message ?? error.toString()}
</>
);
}
return (
<div className={errorLayout}>
<div className={errorDetailStyle}>{errorDetail}</div>
<span className={errorDivider} />
<div
className={errorImage}
style={{ backgroundImage: `url(${errorBackground})` }}
/>
<Provider key="JotaiProvider" store={getCurrentStore()}>
<DumpInfo />
</Provider>
</div>
);
}
return this.props.children;
}
}
@@ -1,114 +0,0 @@
import type {
QueryParamError,
Unreachable,
WorkspaceNotFoundError,
} from '@affine/env/constant';
import { PageNotFoundError } from '@affine/env/constant';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
getCurrentStore,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
import type React from 'react';
import { Component } from 'react';
import { useLocation, useParams } from 'react-router-dom';
export type AffineErrorBoundaryProps = React.PropsWithChildren;
type AffineError =
| QueryParamError
| Unreachable
| WorkspaceNotFoundError
| PageNotFoundError
| Error;
interface AffineErrorBoundaryState {
error: AffineError | null;
}
export const DumpInfo = () => {
const location = useLocation();
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname;
const query = useParams();
return (
<>
<div>
Please copy the following information and send it to the developer.
</div>
<div
style={{
border: '1px solid red',
}}
>
<div>path: {path}</div>
<div>query: {JSON.stringify(query)}</div>
<div>currentWorkspaceId: {currentWorkspaceId}</div>
<div>currentPageId: {currentPageId}</div>
<div>metadata: {JSON.stringify(metadata)}</div>
</div>
</>
);
};
export class AffineErrorBoundary extends Component<
AffineErrorBoundaryProps,
AffineErrorBoundaryState
> {
public override state: AffineErrorBoundaryState = {
error: null,
};
public static getDerivedStateFromError(
error: AffineError
): AffineErrorBoundaryState {
return { error };
}
public override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public override render(): ReactNode {
if (this.state.error) {
let errorDetail: ReactElement | null = null;
const error = this.state.error;
if (error instanceof PageNotFoundError) {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
<>
<span> Page error </span>
<span>
Cannot find page {error.pageId} in workspace{' '}
{error.workspace.id}
</span>
</>
</>
);
} else {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
{error.message ?? error.toString()}
</>
);
}
return (
<>
{errorDetail}
<Provider key="JotaiProvider" store={getCurrentStore()}>
<DumpInfo />
</Provider>
</>
);
}
return this.props.children;
}
}
@@ -0,0 +1,25 @@
<svg width="402" height="178" viewBox="0 0 402 178" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M93.7434 129.308H1L71.7965 15.1021V167.142" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
<ellipse cx="71.4426" cy="14.7483" rx="3.89381" ry="3.88938" fill="#121212" />
<ellipse cx="93.3894" cy="129.308" rx="3.89381" ry="3.88938" fill="#121212" />
<path d="M140.357 27.1235L264.717 151.342" stroke="#E3E2E4" />
<rect x="140.392" y="27.1582" width="124.291" height="124.149" stroke="#E3E2E4" />
<path d="M264.339 89.4652C264.895 85.8242 265.183 82.0954 265.183 78.2995C265.183 37.7024 232.235 4.79199 191.592 4.79199C150.948 4.79199 118 37.7024 118 78.2995C118 118.897 150.948 151.807 191.592 151.807C195.23 151.807 198.807 151.543 202.304 151.034" stroke="#E3E2E4" />
<path d="M202.304 150.964C205.949 151.519 209.682 151.807 213.483 151.807C254.126 151.807 287.074 118.897 287.074 78.2995C287.074 37.7024 254.126 4.79199 213.483 4.79199C172.839 4.79199 139.892 37.7024 139.892 78.2995C139.892 82.0955 140.18 85.8242 140.735 89.4652" stroke="#E3E2E4" />
<path d="M140.2 89.4652C139.69 92.9584 139.426 96.5312 139.426 100.166C139.426 140.763 172.374 173.673 213.017 173.673C253.66 173.673 286.608 140.763 286.608 100.166C286.608 59.5686 253.66 26.6582 213.017 26.6582C209.378 26.6582 205.801 26.922 202.304 27.4314" stroke="#E3E2E4" />
<path d="M202.304 27.4314C198.807 26.922 195.23 26.6582 191.592 26.6582C150.948 26.6582 118 59.5686 118 100.166C118 140.763 150.948 173.673 191.592 173.673C232.235 173.673 265.183 140.763 265.183 100.166C265.183 96.5312 264.919 92.9584 264.409 89.4652" stroke="#E3E2E4" />
<path d="M264.717 27.1235L140.357 151.342" stroke="#E3E2E4" />
<path d="M139.892 89.4653H264.717" stroke="#E3E2E4" />
<path d="M202.304 26.6582V151.807" stroke="#E3E2E4" />
<ellipse cx="264.717" cy="89.4651" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="264.717" cy="27.1233" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="264.717" cy="151.807" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
<path d="M401 127.187H308.257L379.053 12.9805V165.02" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
<ellipse cx="379.407" cy="127.187" rx="3.89381" ry="3.88938" fill="#121212" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB