BREAKING CHANGE: Fix graphql errors (#12775)

We were using a global ValidationPipe in main.ts. This is an issue as
@Controllers should return HttpExecption and @Resolvers should return
GraphqlErrors

Removing the global pipe and creating a ResolverValidationPipe able to
generate GraphqlError. We also need to handle the exception in a filter
to avoid nest to think it's unhandled and make it flow to logs


Next step:
- it would be nice to have both @UsePipes(ResolverValidationPipe) +
@UseFilters(GraphqlValidationExceptionFilter) come together. This should
be possible if we create a @GraphQLResolver annotation
This commit is contained in:
Charles Bochet
2025-06-23 11:23:16 +02:00
committed by GitHub
parent 8e30da99e9
commit b76dac2ca1
36 changed files with 263 additions and 146 deletions
@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@Module({
imports: [ExceptionHandlerModule],
exports: [useGraphQLErrorHandlerHook],
providers: [],
exports: [useGraphQLErrorHandlerHook, ResolverValidationPipe],
providers: [ResolverValidationPipe],
})
export class EngineGraphQLModule {}
@@ -0,0 +1,17 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { GraphQLError } from 'graphql';
/**
* In NestJS, if an exception is not handled, it will shown in the logs
* This filter is used to prevent NestJS from auto-logging GraphQL errors
* and leave it to the GraphQL layer to handle the error.
*/
@Catch(GraphQLError)
export class PreventNestToAutoLogGraphqlErrorsFilter
implements ExceptionFilter
{
catch(exception: GraphQLError, _host: ArgumentsHost) {
return exception;
}
}
@@ -0,0 +1,58 @@
import {
ArgumentMetadata,
Injectable,
PipeTransform,
Type,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { ValidationError, validate } from 'class-validator';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@Injectable()
export class ResolverValidationPipe implements PipeTransform {
async transform(value: unknown, metadata: ArgumentMetadata) {
const { metatype } = metadata;
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
try {
const errors = await validate(object);
if (errors.length > 0) {
const errorMessage = this.formatErrorMessage(errors);
throw new UserInputError(errorMessage);
}
} catch (error) {
// If the element is not a class, we can't validate it
return value;
}
return value;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private toValidate(metatype: Type<any>): boolean {
const types: unknown[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
private formatErrorMessage(errors: ValidationError[]): string {
const messages = errors.flatMap((error) => {
if (error.constraints) {
return Object.values(error.constraints);
}
return [];
});
return messages.join(', ');
}
}