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:
liuyi
2024-01-31 08:43:03 +00:00
parent 72d9cc1e5b
commit 26db1d436d
22 changed files with 310 additions and 193 deletions
@@ -4,7 +4,7 @@ import {
GraphQLRequestListener,
} from '@apollo/server';
import { Plugin } from '@nestjs/apollo';
import { Logger } from '@nestjs/common';
import { HttpException, Logger } from '@nestjs/common';
import { Response } from 'express';
import { metrics } from '../metrics/metrics';
@@ -27,28 +27,44 @@ export class GQLLoggerPlugin implements ApolloServerPlugin {
metrics.gql.counter('query_counter').add(1, { operation });
const start = Date.now();
function endTimer() {
return Date.now() - start;
}
return Promise.resolve({
willSendResponse: () => {
const costInMilliseconds = Date.now() - start;
res.setHeader(
'Server-Timing',
`gql;dur=${costInMilliseconds};desc="GraphQL"`
);
metrics.gql
.histogram('query_duration')
.record(costInMilliseconds, { operation });
const time = endTimer();
res.setHeader('Server-Timing', `gql;dur=${time};desc="GraphQL"`);
metrics.gql.histogram('query_duration').record(time, { operation });
return Promise.resolve();
},
didEncounterErrors: () => {
const costInMilliseconds = Date.now() - start;
res.setHeader(
'Server-Timing',
`gql;dur=${costInMilliseconds};desc="GraphQL ${operation}"`
);
metrics.gql
.histogram('query_duration')
.record(costInMilliseconds, { operation });
didEncounterErrors: ctx => {
metrics.gql.counter('query_error_counter').add(1, { operation });
ctx.errors.forEach(err => {
// only log non-user errors
let msg: string | undefined;
if (!err.originalError) {
msg = err.toString();
} else {
const originalError = err.originalError;
// do not log client errors, and put more information in the error extensions.
if (!(originalError instanceof HttpException)) {
if (originalError.cause && originalError.cause instanceof Error) {
msg = originalError.cause.stack ?? originalError.cause.message;
} else {
msg = originalError.stack ?? originalError.message;
}
}
}
if (msg) {
this.logger.error('GraphQL Unhandled Error', msg);
}
});
return Promise.resolve();
},
});