refactor(server): server errors (#5741)
standardize the error raising in both GraphQL Resolvers and Controllers.
Now, All user aware errors should be throwed with `HttpException`'s variants, for example `NotFoundException`.
> Directly throwing `GraphQLError` are forbidden.
The GraphQL errorFormatter will handle it automatically and set `code`, `status` in error extensions.
At the same time, the frontend `GraphQLError` should be imported from `@affine/graphql`, which introduce a better error extensions type.
----
controller example:
```js
@Get('/docs/${id}')
doc() {
// ...
// imported from '@nestjs/common'
throw new NotFoundException('Doc is not found.');
// ...
}
```
the above will response as:
```
status: 404 Not Found
{
"message": "Doc is not found.",
"statusCode": 404,
"error": "Not Found"
}
```
resolver example:
```js
@Mutation()
invite() {
// ...
throw new PayloadTooLargeException('Workspace seats is full.')
// ...
}
```
the above will response as:
```
status: 200 Ok
{
"data": null,
"errors": [
{
"message": "Workspace seats is full.",
"extensions": {
"code": 404,
"status": "Not Found"
}
}
]
}
```
for frontend GraphQLError user-friend, a helper function introduced:
```js
import { findGraphQLError } from '@affine/graphql'
fetch(query)
.catch(errOrArr => {
const e = findGraphQLError(errOrArr, e => e.extensions.code === 404)
if (e) {
// handle
}
})
```
This commit is contained in:
@@ -5,11 +5,14 @@ import {
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { type GetUserQuery, getUserQuery } from '@affine/graphql';
|
||||
import {
|
||||
findGraphQLError,
|
||||
type GetUserQuery,
|
||||
getUserQuery,
|
||||
} from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -64,8 +67,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
const user: GetUserQuery['user'] | null | 0 = await verifyUser({ email })
|
||||
.then(({ user }) => user)
|
||||
.catch(err => {
|
||||
const e = err?.[0];
|
||||
if (e instanceof GraphQLError && e.extensions?.code === 402) {
|
||||
if (findGraphQLError(err, e => e.extensions.code === 402)) {
|
||||
setAuthState('noAccess');
|
||||
return 0;
|
||||
} else {
|
||||
|
||||
26
packages/frontend/graphql/src/error.ts
Normal file
26
packages/frontend/graphql/src/error.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { GraphQLError as BaseGraphQLError } from 'graphql';
|
||||
import { identity } from 'lodash-es';
|
||||
|
||||
interface KnownGraphQLErrorExtensions {
|
||||
code: number;
|
||||
status: string;
|
||||
originalError?: unknown;
|
||||
stacktrace?: string;
|
||||
}
|
||||
|
||||
export class GraphQLError extends BaseGraphQLError {
|
||||
// @ts-expect-error better to be a known type without any type casting
|
||||
override extensions!: KnownGraphQLErrorExtensions;
|
||||
}
|
||||
export function findGraphQLError(
|
||||
errOrArr: any,
|
||||
filter: (err: GraphQLError) => boolean = identity
|
||||
): GraphQLError | undefined {
|
||||
if (errOrArr instanceof GraphQLError) {
|
||||
return filter(errOrArr) ? errOrArr : undefined;
|
||||
} else if (Array.isArray(errOrArr)) {
|
||||
return errOrArr.find(err => err instanceof GraphQLError && filter(err));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './error';
|
||||
export * from './fetcher';
|
||||
export * from './graphql';
|
||||
export * from './schema';
|
||||
@@ -18,5 +19,3 @@ export function getBaseUrl(): string {
|
||||
}
|
||||
|
||||
export const fetcher = gqlFetcherFactory(getBaseUrl() + '/graphql');
|
||||
|
||||
export { GraphQLError } from 'graphql';
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import {
|
||||
deleteBlobMutation,
|
||||
fetchWithTraceReport,
|
||||
findGraphQLError,
|
||||
getBaseUrl,
|
||||
GraphQLError,
|
||||
listBlobsQuery,
|
||||
setBlobMutation,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/graphql';
|
||||
import { type BlobStorage, BlobStorageOverCapacity } from '@toeverything/infra';
|
||||
import { isArray } from 'lodash-es';
|
||||
|
||||
import { bufferToBlob } from '../utils/buffer-to-blob';
|
||||
|
||||
@@ -43,13 +42,15 @@ export class AffineCloudBlobStorage implements BlobStorage {
|
||||
})
|
||||
.then(res => res.setBlob)
|
||||
.catch(err => {
|
||||
if (isArray(err)) {
|
||||
err.map(e => {
|
||||
if (e instanceof GraphQLError && e.extensions.code === 413) {
|
||||
throw new BlobStorageOverCapacity(e);
|
||||
} else throw e;
|
||||
});
|
||||
const uploadError = findGraphQLError(
|
||||
err,
|
||||
e => e.extensions.code === 413
|
||||
);
|
||||
|
||||
if (uploadError) {
|
||||
throw new BlobStorageOverCapacity(uploadError);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user