🔕

GraphQL Nest.js ExceptionFilter レスポンスが返らずタイムアウトになる場合

2023/11/30に公開

概要

  • 公式ドキュメントの実装だと、クライアントからのリクエストによってはレスポンスが返らずにタイムアウトになってしまうことがあります。
  • この状態になるのは、execution typeがgraphql と見なされないときで、リクエストボディのjsonが壊れているときなどに起こります。
  • execution typeがgraphqlでない時用の実装をException Filterに追加すれば解決できます。

[バージョンなど]

  • @nestjs/core: ^9.0.0
  • @nestjs/graphql: ^11.0.5

タイムアウトの再現

ExceptionFilterを公式ドキュメント通りに用意します。

@Catch(HttpException)
export class HttpExceptionFilter implements GqlExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    console.error('exception filter', exception);  // ここは動作確認用に追加
    const gqlHost = GqlArgumentsHost.create(host);
    return exception;
  }
}

main.ts にFilterを追加して、サーバを起動します。

main.ts
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}

リクエストボディのJSONを壊してリクエストを送信します。
下記の例では、variables{"id": "test"}{"id": test} としています。

curl --location 'http://localhost:3000/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"query foo($id: Int!) {foo {id}}","variables":{"id":test}}'

すると、JSONのparseエラーが起き、レスポンスが返らずにタイムアウトしてしまいます。

ExceptionErrorのconsole.errorの出力
exception filter BadRequestException: Unexpected token e in JSON at position 63
    at RoutesResolver.mapExternalException ... (中略)
response: {
    statusCode: 400,
    message: 'Unexpected token e in JSON at position 63',
    error: 'Bad Request'
  },
  status: 400,
  options: {}
}

これはリクエストボディのJSONが壊れていることで、GraphQLのリクエストとして解釈されず、正常に処理されないことが原因のようです。

対策

通常のhttpのリクエスト用の処理を追加し、レスポンスを返すようにすればOK

import { Response } from 'express';  // これを追加


@Catch(HttpException)
export class HttpExceptionFilter implements GqlExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctxType = host.getType<GqlContextType>();
    
    if (ctxType !== 'graphql') {
      const ctx = host.switchToHttp();
      const res = ctx.getResponse<Response>();
      const status = exception.getStatus();
      
      response.status(200).json({
        errors: [
          {
            message: 'JSON parse error',
            extensions: {
              code: status,
            },
          },
        ],
      });
    }
    
    return exception;
  }
}

これで、リクエストのJSONが不正でもエラーレスポンスが返るようになります

{
    "errors": [
        {
            "message": "JSON parse error",
            "extensions": {
                "code": 400
            }
        }
    ]
}
株式会社Poksha

Discussion