mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(server): introduce user friendly server errors (#7111)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user