nestjs / graphql

GraphQL (TypeScript) module for Nest framework (node.js) 🍷
https://docs.nestjs.com/graphql/quick-start
MIT License
1.46k stars 396 forks source link

response.status is not a function when filtering GraphQL exceptions #2454

Closed scoussens closed 2 years ago

scoussens commented 2 years ago

Is there an existing issue for this?

Current behavior

In this particular case I'm using Prisma as the ORM, and then occurs when Prisma throws an exception that is then caught by an exception Filter.

import { PrismaClientExceptionFilter, PrismaService } from 'nestjs-prisma';

import { AppModule } from './app.module';

import type {
  CorsConfig,
  NestConfig,
  SwaggerConfig,
} from 'src/common/configs/config.interface';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Validation
  app.useGlobalPipes(new ValidationPipe());

  // enable shutdown hook
  const prismaService: PrismaService = app.get(PrismaService);
  await prismaService.enableShutdownHooks(app);

  // Prisma Client Exception Filter for unhandled exceptions
  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter));

The issue though, does not appear to be related to Prisma, but rather that there is an issue with the GraphQL context object that is being passed into the Filter. Using a custom exception shows the behavior when debugging.

import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Response } from 'express';

@Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaClientExceptionFilter implements ExceptionFilter {
  logger = new Logger('PrismaClientExceptionFilter');

  catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();

When you view the context for this object it first is not properly switched to the HTTP context, but second the ctx.getResponse function is actually just an index retrieval function for what should be the Reponse property on the object ( getArgByIndex(1) ).

In the GraphQL context this just returns the GraphQL request payload ( { where: { id: 1 }, data: { update: 'something' } } ). image

Looking at the CTX from the host (type is graphql), it shows there is an 'undefined' argument as arg 0, and I believe this might be throwing it off? image

You can reproduce this issue using the Prisma-NestJS-Starter repository, and simply change the 'updateUser' function in the user service to have Prisma attempt to update a user that doesn't exist.

image

This will throw the error listed in the title of this issue.

Minimum reproduction code

https://github.com/notiz-dev/nestjs-prisma-starter

Steps to reproduce

You can reproduce this issue using the Prisma-NestJS-Starter repository, and simply change the 'updateUser' function in the user service to have Prisma attempt to update a user that doesn't exist.

image

This will throw the error listed in the title of this issue.

Expected behavior

You should be able to access the response object in order to send back a properly formatted error message for the captured error.

Package version

10.1.3

Graphql version

graphql: apollo-server-express: apollo-server-fastify:

NestJS version

9.1.4

Node.js version

16.16

In which operating systems have you tested?

Other

No response

jmcdo29 commented 2 years ago

You're using a GraphQL server (for the most part), so you should get the GqlArgumentsHost and get the response from GqlArguemtnsHost.create(host).getContext().res rather than host.switchToHttp().getResponse()

scoussens commented 2 years ago

Thanks. This must have changed at some point since all the examples use the information above and the nestjs-prisma package itself no longer works for this.

Here is the fix for me in this case that worked.

Change the filter to look like the following for getting the response:

catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
    const gqlHost = GqlArgumentsHost.create(host).getContext();
    const res = gqlHost.res as Response;

Then you have to make sure and update the configuration for the GraphQL server for NestJS to also supply the response object (only does request by default):

export class GqlConfigService implements GqlOptionsFactory {
  constructor(private configService: ConfigService) {}
  createGqlOptions(): ApolloDriverConfig {
    const graphqlConfig = this.configService.get<GraphqlConfig>('graphql');
    return {
      // schema options
      autoSchemaFile: graphqlConfig.schemaDestination || './src/schema.graphql',
      sortSchema: graphqlConfig.sortSchema,
      buildSchemaOptions: {
        numberScalarMode: 'integer',
      },
      // subscription
      subscriptions: {
        'graphql-ws': true,
      },
      debug: graphqlConfig.debug,
      playground: graphqlConfig.playgroundEnabled,
// added the response object here, initial example only had request
      context: ({ req, res }) => ({ req, res }),
    };
  }
}
jmcdo29 commented 2 years ago

all the examples use the information above and the nestjs-prisma package itself

Most likely they run off of REST requests, which your original filter would be fine for. It all depends on the transport context