Open9

NestJSのExceptionFilter調査

み

まずはErrorからカスタムエラーを作ってやってみる

error.ts
export class ApplicationException extends Error {
  constructor(public readonly message: string) {
    super("Application Error");
  }
}
filter.ts
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  Logger,
} from "@nestjs/common";
import { GqlArgumentsHost, GqlExceptionFilter } from "@nestjs/graphql";
import {
  ApplicationError,
  NotFoundError,
  ApplicationException,
} from "src/common/errors";

@Catch(ApplicationException)
export class ApiExceptionFilter implements GqlExceptionFilter {
  private readonly logger = new Logger(ApiExceptionFilter.name);

  // catch(exception: ApplicationError, host: ArgumentsHost) {
  catch(exception: ApplicationException, host: ArgumentsHost) {
    const gqlHost = GqlArgumentsHost.create(host);
    const context = gqlHost.getContext();
    const info = gqlHost.getInfo();
    // ... custom logic
    return exception;
{
  "errors": [
    {
      "message": "App exception",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createCategory"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "Error: App exception",
         ...
        ]
      }
    }
  ],
  "data": null
}

普通にreturn exception;だとGraphQLトップレベルerrorsで返る。
ドメインエラーを補足してdata.errorsに詰め直すジェネリックな関数がほしい

み
filter.ts
  return null;
{
  "errors": [
    {
      "message": "App exception",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createCategory"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "Error: App exception",
          ...
        ]
      }
    }
  ],
  "data": null
}
み
fitler.ts
  return {
      category: null,
      errors: [new ApplicationError("New error", "New_ERROR")],
    };
{
  "errors": [
    {
      "message": "Abstract type \"CategoryError\" must resolve to an Object type at runtime for field \"CategoryPayload.errors\". Either the \"CategoryError\" type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.",
      "locations": [
        {
          "line": 10,
          "column": 5
        }
      ],
      "path": [
        "createCategory",
        "errors",
        0
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "GraphQLError: Abstract type \"CategoryError\" must resolve to an Object type at runtime for field \"CategoryPayload.errors\". Either the \"CategoryError\" type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.",
          ...
        ]
      }
    }
  ],
  "data": {
    "createCategory": {
      "category": null,
      "errors": null
    }
  }
}
み

CategoryErrorをなおしたら行けた

export const CategoryError = createUnionType({
  name: "CategoryError",
  types: () =>
    [NotFoundError, InputError, ValidationError, ApplicationError] as const,
  resolveType(value) {
    if (value instanceof NotFoundError) return NotFoundError;
    if (value instanceof InputError) return InputError;
    if (value instanceof ValidationError) return ValidationError;
    if (value instanceof ApplicationError) return ApplicationError;
    return null;
  },
});
{
  "data": {
    "createCategory": {
      "category": null,
      "errors": [
        {
          "__typename": "ApplicationError",
          "message": "New error",
          "code": "New_ERROR"
        }
      ]
    }
  }
}
み

標準Errorではなくカスタムオブジェクトから定義したオブジェクトでも動くか確認。

error.ts
import { Field, ObjectType } from "@nestjs/graphql";

@ObjectType()
export class BaseError {
  @Field()
  message: string;

  @Field()
  code: string;
}

@ObjectType()
export class ApplicationError extends BaseError {
  constructor(message: string, code: string) {
    super();
    this.message = message;
    this.code = code;
  }
}

@ObjectType()
export class NotFoundError extends BaseError {
  constructor(resource: string) {
    super();
    this.message = `${resource} not found`;
    this.code = "NOT_FOUND";
  }
}
filter.ts
@Catch(ApplicationError)
// @Catch(ApplicationException)
export class ApiExceptionFilter implements GqlExceptionFilter {
  private readonly logger = new Logger(ApiExceptionFilter.name);

  catch(exception: ApplicationError, host: ArgumentsHost) {
    // catch(exception: ApplicationException, host: ArgumentsHost) {
    const gqlHost = GqlArgumentsHost.create(host);
    const context = gqlHost.getContext();
    const info = gqlHost.getInfo();
    return {
      category: null,
      errors: [new NotFoundError("New error")],
    };
{
  "data": {
    "createCategory": {
      "category": null,
      "errors": [
        {
          "__typename": "NotFoundError",
          "message": "New error not found",
          "code": "NOT_FOUND"
        }
      ]
    }
  }
}

ApplicationErrorを捕捉してNotFoundErrorに詰め替えてるけど、ちゃんとできた

み

returnのところでnewせずにオブジェクトだけ渡すと怒られる。

filter.ts
    return {
      category: null,
      errors: [
        {
          message: exception.message,
          code: "APP_EXCEPTION",
        },
      ],
    };
{
  "errors": [
    {
      "message": "Abstract type \"CategoryError\" must resolve to an Object type at runtime for field \"CategoryPayload.errors\". Either the \"CategoryError\" type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.",
      "locations": [
        {
          "line": 10,
          "column": 5
        }
      ],
      "path": [
        "createCategory",
        "errors",
        0
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "GraphQLError: Abstract type \"CategoryError\" must resolve to an Object type at runtime for field \"CategoryPayload.errors\". Either the \"CategoryError\" type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.",
          ...
        ]
      }
    }
  ],
  "data": {
    "createCategory": {
      "category": null,
      "errors": null
    }
  }
}
み

捕捉したexceptionをそのまま詰めれば当然OK

filter.ts
    return {
      category: null,
      errors: [exception],
    };
{
  "data": {
    "createCategory": {
      "category": null,
      "errors": [
        {
          "__typename": "ApplicationError",
          "message": "App error",
          "code": "APP_ERROR"
        }
      ]
    }
  }
}
み

あとは戻りの型を取得してその形に詰め替えてあげれば行けそうな予感

み

その前に、ただreturnするケースもやってみた

filter.ts
return;
{
  "errors": [
    {
      "message": "Unexpected error value: { message: \"App error\", code: \"APP_ERROR\" }",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createCategory"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "NonErrorThrown: Unexpected error value: { message: \"App error\", code: \"APP_ERROR\" }",
          ...
        ]
      }
    }
  ],
  "data": null
}