fix: sentry doesn't catch exceptions from flexible backend (#3074)

* fix: sentry doesn't catch exceptions from flexible backend

* fix: send remaining errors to Sentry

* fix: missing debug

* feat: use an util exception handler instead of Nest.JS class
This commit is contained in:
Jérémy M
2023-12-20 12:04:59 +01:00
committed by GitHub
parent ed7bd0ba26
commit d59a37129f
6 changed files with 179 additions and 151 deletions

View File

@ -1,19 +1,12 @@
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ConfigModule } from '@nestjs/config';
import { APP_FILTER, ContextIdFactory, ModuleRef } from '@nestjs/core';
import { APP_FILTER } from '@nestjs/core';
import { GraphQLError, GraphQLSchema } from 'graphql';
import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs';
import GraphQLJSON from 'graphql-type-json';
import { TokenExpiredError, JsonWebTokenError } from 'jsonwebtoken';
import { WorkspaceFactory } from 'src/workspace/workspace.factory';
import { TypeOrmExceptionFilter } from 'src/filters/typeorm-exception.filter';
import { HttpExceptionFilter } from 'src/filters/http-exception.filter';
import { GraphQLConfigService } from 'src/graphql-config.service';
import { GlobalExceptionFilter } from 'src/filters/global-exception.filter';
import { TokenService } from 'src/core/auth/services/token.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { AppService } from './app.service';
@ -27,61 +20,10 @@ import { WorkspaceModule } from './workspace/workspace.module';
ConfigModule.forRoot({
isGlobal: true,
}),
GraphQLModule.forRoot<YogaDriverConfig>({
context: ({ req }) => ({ req }),
GraphQLModule.forRootAsync<YogaDriverConfig>({
driver: YogaDriver,
autoSchemaFile: true,
include: [CoreModule],
conditionalSchema: async (request) => {
try {
// Get TokenService from AppModule
const tokenService = AppModule.moduleRef.get(TokenService, {
strict: false,
});
let workspace: Workspace;
try {
workspace = await tokenService.validateToken(request.req);
} catch (err) {
return new GraphQLSchema({});
}
const contextId = ContextIdFactory.create();
AppModule.moduleRef.registerRequestByContextId(request, contextId);
// Get the SchemaGenerationService from the AppModule
const workspaceFactory = await AppModule.moduleRef.resolve(
WorkspaceFactory,
contextId,
{
strict: false,
},
);
return await workspaceFactory.createGraphQLSchema(workspace.id);
} catch (error) {
if (error instanceof JsonWebTokenError) {
//mockedUserJWT
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (error instanceof TokenExpiredError) {
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
throw error;
}
},
resolvers: { JSON: GraphQLJSON },
plugins: [],
imports: [CoreModule],
useClass: GraphQLConfigService,
}),
HealthModule,
IntegrationsModule,
@ -90,27 +32,10 @@ import { WorkspaceModule } from './workspace/workspace.module';
],
providers: [
AppService,
// Exceptions filters must be ordered from the least specific to the most specific
// If TypeOrmExceptionFilter handle something, HttpExceptionFilter will not handle it
// GlobalExceptionFilter will handle the rest of the exceptions
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
{
provide: APP_FILTER,
useClass: TypeOrmExceptionFilter,
useValue: GlobalExceptionFilter,
},
],
})
export class AppModule {
static moduleRef: ModuleRef;
constructor(private moduleRef: ModuleRef) {
AppModule.moduleRef = this.moduleRef;
}
}
export class AppModule {}

View File

@ -1,6 +1,7 @@
import { Catch, Injectable } from '@nestjs/common';
import { GqlExceptionFilter } from '@nestjs/graphql';
import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
@Catch()
@ -11,8 +12,6 @@ export class GlobalExceptionFilter implements GqlExceptionFilter {
) {}
catch(exception: unknown) {
this.exceptionHandlerService.captureException(exception);
return exception;
return globalExceptionHandler(exception, this.exceptionHandlerService);
}
}

View File

@ -1,42 +0,0 @@
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
import {
AuthenticationError,
BaseGraphQLError,
ForbiddenError,
} from 'src/filters/utils/graphql-errors.util';
const graphQLPredefinedExceptions = {
401: AuthenticationError,
403: ForbiddenError,
};
@Catch(HttpException)
export class HttpExceptionFilter
implements GqlExceptionFilter<HttpException, BaseGraphQLError | null>
{
catch(exception: HttpException, host: ArgumentsHost) {
if (host.getType<GqlContextType>() !== 'graphql') {
return null;
}
let error: BaseGraphQLError;
if (exception.getStatus() in graphQLPredefinedExceptions) {
error = new graphQLPredefinedExceptions[exception.getStatus()](
exception.message,
);
} else {
error = new BaseGraphQLError(
exception.message,
exception.getStatus().toString(),
);
}
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
return error;
}
}

View File

@ -1,24 +0,0 @@
import { ArgumentsHost, Catch } from '@nestjs/common';
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
import { TypeORMError } from 'typeorm';
import { BaseGraphQLError } from 'src/filters/utils/graphql-errors.util';
@Catch(TypeORMError)
export class TypeOrmExceptionFilter
implements GqlExceptionFilter<TypeORMError, BaseGraphQLError | null>
{
catch(exception: TypeORMError, host: ArgumentsHost) {
if (host.getType<GqlContextType>() !== 'graphql') {
return null;
}
const error = new BaseGraphQLError(exception.name, 'INTERNAL_SERVER_ERROR');
error.stack = exception.stack;
error.extensions['response'] = exception.message;
return error;
}
}

View File

@ -0,0 +1,75 @@
import { HttpException } from '@nestjs/common';
import { TypeORMError } from 'typeorm';
import {
AuthenticationError,
BaseGraphQLError,
ForbiddenError,
} from 'src/filters/utils/graphql-errors.util';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
const graphQLPredefinedExceptions = {
401: AuthenticationError,
403: ForbiddenError,
};
export const globalExceptionHandler = (
exception: unknown,
exceptionHandlerService: ExceptionHandlerService,
) => {
if (exception instanceof HttpException) {
return httpExceptionHandler(exception, exceptionHandlerService);
}
if (exception instanceof TypeORMError) {
return typeOrmExceptionHandler(exception, exceptionHandlerService);
}
exceptionHandlerService.captureException(exception);
return exception;
};
export const httpExceptionHandler = (
exception: HttpException,
exceptionHandlerService: ExceptionHandlerService,
) => {
const status = exception.getStatus();
let error: BaseGraphQLError;
// Capture all 5xx errors and send them to exception handler
if (status >= 500) {
exceptionHandlerService.captureException(exception);
}
if (status in graphQLPredefinedExceptions) {
error = new graphQLPredefinedExceptions[exception.getStatus()](
exception.message,
);
} else {
error = new BaseGraphQLError(
exception.message,
exception.getStatus().toString(),
);
}
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
return error;
};
export const typeOrmExceptionHandler = (
exception: TypeORMError,
exceptionHandlerService: ExceptionHandlerService,
) => {
exceptionHandlerService.captureException(exception);
const error = new BaseGraphQLError(exception.name, 'INTERNAL_SERVER_ERROR');
error.stack = exception.stack;
error.extensions['response'] = exception.message;
return error;
};

View File

@ -0,0 +1,95 @@
import { Injectable } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { GqlOptionsFactory } from '@nestjs/graphql';
import {
YogaDriverConfig,
YogaDriverServerContext,
} from '@graphql-yoga/nestjs';
import { GraphQLSchema, GraphQLError } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
import { TokenService } from 'src/core/auth/services/token.service';
import { CoreModule } from 'src/core/core.module';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { WorkspaceFactory } from 'src/workspace/workspace.factory';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util';
@Injectable()
export class GraphQLConfigService
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
{
constructor(
private readonly tokenService: TokenService,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly moduleRef: ModuleRef,
) {}
createGqlOptions(): YogaDriverConfig {
return {
context: ({ req }) => ({ req }),
autoSchemaFile: true,
include: [CoreModule],
conditionalSchema: async (context) => {
try {
let workspace: Workspace;
// If token is not valid, it will return an empty schema
try {
workspace = await this.tokenService.validateToken(context.req);
} catch (err) {
return new GraphQLSchema({});
}
return await this.createSchema(context, workspace);
} catch (error) {
if (error instanceof JsonWebTokenError) {
//mockedUserJWT
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (error instanceof TokenExpiredError) {
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
throw globalExceptionHandler(error, this.exceptionHandlerService);
}
},
resolvers: { JSON: GraphQLJSON },
plugins: [],
};
}
async createSchema(
context: YogaDriverServerContext<'express'> & YogaInitialContext,
workspace: Workspace,
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
// Create a new contextId for each request
const contextId = ContextIdFactory.create();
// Register the request in the contextId
this.moduleRef.registerRequestByContextId(context.req, contextId);
// Resolve the WorkspaceFactory for the contextId
const workspaceFactory = await this.moduleRef.resolve(
WorkspaceFactory,
contextId,
{
strict: false,
},
);
return await workspaceFactory.createGraphQLSchema(workspace.id);
}
}