feat(server): introduce user friendly server errors (#7111)

This commit is contained in:
liuyi
2024-06-17 11:30:58 +08:00
committed by GitHub
parent 5307a55f8a
commit 54fc1197ad
65 changed files with 3170 additions and 924 deletions

View File

@@ -11,6 +11,7 @@ import {
type GraphQLQuery,
type QueryOptions,
type RequestOptions,
UserFriendlyError,
} from '@affine/graphql';
import {
GeneralNetworkError,
@@ -33,27 +34,16 @@ function codeToError(code: number) {
}
}
type ErrorType =
| GraphQLError[]
| GraphQLError
| { status: number }
| Error
| string;
export function resolveError(err: any) {
const standardError =
err instanceof GraphQLError
? new UserFriendlyError(err.extensions)
: UserFriendlyError.fromAnyError(err);
export function resolveError(src: ErrorType) {
if (typeof src === 'string') {
return new GeneralNetworkError(src);
} else if (src instanceof GraphQLError || Array.isArray(src)) {
// only resolve the first error
const error = Array.isArray(src) ? src.at(0) : src;
const code = error?.extensions?.code;
return codeToError(code ?? 500);
} else {
return codeToError(src instanceof Error ? 500 : src.status);
}
return codeToError(standardError.status);
}
export function handleError(src: ErrorType) {
export function handleError(src: any) {
const err = resolveError(src);
if (err instanceof UnauthorizedError) {
getCurrentStore().set(showAILoginRequiredAtom, true);
@@ -66,8 +56,7 @@ const fetcher = async <Query extends GraphQLQuery>(
) => {
try {
return await defaultFetcher<Query>(options);
} catch (_err) {
const err = _err as GraphQLError | GraphQLError[] | Error | string;
} catch (err) {
throw handleError(err);
}
};

View File

@@ -1,4 +1,5 @@
import { DebugLogger } from '@affine/debug';
import { UserFriendlyError } from '@affine/graphql';
import { fromPromise, Service } from '@toeverything/infra';
import { BackendError, NetworkError } from '../error';
@@ -75,9 +76,7 @@ export class FetchService extends Service {
// ignore
}
}
throw new BackendError(
new Error(`${res.status} ${res.statusText}`, reason)
);
throw new BackendError(UserFriendlyError.fromAnyError(reason));
}
return res;
};

View File

@@ -1,9 +1,9 @@
import {
gqlFetcherFactory,
GraphQLError,
type GraphQLQuery,
type QueryOptions,
type QueryResponse,
UserFriendlyError,
} from '@affine/graphql';
import { fromPromise, Service } from '@toeverything/infra';
import type { Observable } from 'rxjs';
@@ -39,15 +39,13 @@ export class GraphQLService extends Service {
try {
return await this.rawGql(options);
} catch (err) {
if (err instanceof Array) {
for (const error of err) {
if (error instanceof GraphQLError && error.extensions?.code === 403) {
this.framework.get(AuthService).session.revalidate();
}
}
throw new BackendError(new Error('Graphql Error'));
const standardError = UserFriendlyError.fromAnyError(err);
if (standardError.status === 403) {
this.framework.get(AuthService).session.revalidate();
}
throw err;
throw new BackendError(standardError);
}
};
}

View File

@@ -1,10 +1,10 @@
import {
deleteBlobMutation,
fetcher,
findGraphQLError,
getBaseUrl,
listBlobsQuery,
setBlobMutation,
UserFriendlyError,
} from '@affine/graphql';
import type { BlobStorage } from '@toeverything/infra';
import { BlobStorageOverCapacity } from '@toeverything/infra';
@@ -44,13 +44,9 @@ export class CloudBlobStorage implements BlobStorage {
})
.then(res => res.setBlob)
.catch(err => {
const uploadError = findGraphQLError(
err,
e => e.extensions.code === 413
);
if (uploadError) {
throw new BlobStorageOverCapacity(uploadError);
const error = UserFriendlyError.fromAnyError(err);
if (error.status === 413) {
throw new BlobStorageOverCapacity(error);
}
throw err;

View File

@@ -1,4 +1,9 @@
import { DebugLogger } from '@affine/debug';
import {
ErrorNames,
UserFriendlyError,
type UserFriendlyErrorResponse,
} from '@affine/graphql';
import type { DocServer } from '@toeverything/infra';
import { throwIfAborted } from '@toeverything/infra';
import type { Socket } from 'socket.io-client';
@@ -9,6 +14,8 @@ import { base64ToUint8Array, uint8ArrayToBase64 } from '../../utils/base64';
const logger = new DebugLogger('affine-cloud-doc-engine-server');
type WebsocketResponse<T> = { error: UserFriendlyErrorResponse } | { data: T };
export class CloudDocEngineServer implements DocServer {
interruptCb: ((reason: string) => void) | null = null;
SEND_TIMEOUT = 30000;
@@ -31,21 +38,24 @@ export class CloudDocEngineServer implements DocServer {
const stateVector = state ? await uint8ArrayToBase64(state) : undefined;
const response:
| { error: any }
| { data: { missing: string; state: string; timestamp: number } } =
await this.socket.timeout(this.SEND_TIMEOUT).emitWithAck('doc-load-v2', {
const response: WebsocketResponse<{
missing: string;
state: string;
timestamp: number;
}> = await this.socket
.timeout(this.SEND_TIMEOUT)
.emitWithAck('doc-load-v2', {
workspaceId: this.workspaceId,
guid: docId,
stateVector,
});
if ('error' in response) {
// TODO: result `EventError` with server
if (response.error.code === 'DOC_NOT_FOUND') {
const error = new UserFriendlyError(response.error);
if (error.name === ErrorNames.DOC_NOT_FOUND) {
return null;
} else {
throw new Error(response.error.message);
throw error;
}
} else {
return {
@@ -60,11 +70,7 @@ export class CloudDocEngineServer implements DocServer {
async pushDoc(docId: string, data: Uint8Array) {
const payload = await uint8ArrayToBase64(data);
const response: {
// TODO: reuse `EventError` with server
error?: any;
data: { timestamp: number };
} = await this.socket
const response: WebsocketResponse<{ timestamp: number }> = await this.socket
.timeout(this.SEND_TIMEOUT)
.emitWithAck('client-update-v2', {
workspaceId: this.workspaceId,
@@ -72,38 +78,34 @@ export class CloudDocEngineServer implements DocServer {
updates: [payload],
});
// TODO: raise error with different code to users
if (response.error) {
if ('error' in response) {
logger.error('client-update-v2 error', {
workspaceId: this.workspaceId,
guid: docId,
response,
});
throw new Error(response.error);
throw new UserFriendlyError(response.error);
}
return { serverClock: response.data.timestamp };
}
async loadServerClock(after: number): Promise<Map<string, number>> {
const response: {
// TODO: reuse `EventError` with server
error?: any;
data: Record<string, number>;
} = await this.socket
.timeout(this.SEND_TIMEOUT)
.emitWithAck('client-pre-sync', {
workspaceId: this.workspaceId,
timestamp: after,
});
const response: WebsocketResponse<Record<string, number>> =
await this.socket
.timeout(this.SEND_TIMEOUT)
.emitWithAck('client-pre-sync', {
workspaceId: this.workspaceId,
timestamp: after,
});
if (response.error) {
if ('error' in response) {
logger.error('client-pre-sync error', {
workspaceId: this.workspaceId,
response,
});
throw new Error(response.error);
throw new UserFriendlyError(response.error);
}
return new Map(Object.entries(response.data));