かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Express GraphQL サーバーから返すエラーに独自フィールドを追加したい

Express (TypeScript) で作成した GraphQL サーバーがエラーを返す際に message 以外の独自フィールドを追加してみたメモ

  • express@4.18.1
  • graphql@15.8.0
  • express-graphql@0.12.0
  • typescript@4.7.2

Express GraphQL サーバー

Mutation で username を引数にした login にクエリを投げ username が存在すればログインした体で username を返し、存在しなければエラーを返すだけの簡単な GraphQL サーバー

// server.ts
import express, { Request } from 'express';
import { graphqlHTTP } from 'express-graphql';
import { buildSchema, GraphQLError } from 'graphql';
// Schema
const schema = buildSchema(`
  type Mutation {
    login(username: String!): User
  }
  type User {
    username: String!
  }
`);
// Resolver
const root = {
  login: ({ username }: { username: string }) => {
    if (username === '') {
      throw new Error('User Name is required!');
    }

    return { username };
  },
};

const app = express();
app.use(
  '/graphql',
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
  }),
);
app.listen(4000);
);

サーバーを起動して localhost:4000/graphql にアクセスして表示される GraphiQL の画面で下記のようなクエリを発行してレスポンスが帰ってくればOK

mutation Login($username: String!) {
  login(username: $username) {
    username
  }
}
# Query Variables
{
  "username": "Hoshimiya Ichigo"
}

👇 Response

{
  "data": {
    "login": {
      "username": "Hoshimiya Ichigo"
    }
  }
}

username が空文字の場合はエラー

# Query Variables
{
  "username": ""
}

👇 Response

{
  "errors": [
    {
      "message": "User Name is required!",
      "locations": [ {} ],
      "path": ["login"]
    }
  ],
  "data": {
    "login": null
  }
}

Resolver から throw するエラーに独自のフィールドを持たせる

username が空文字の時に返すエラーに errorType: string という独自フィールドを付けて返したい!

GraphQLError クラスを拡張した独自のエラー型を作成する

express-graphql ではエラーの際に graphql の GraphQLError をエラーとして返しているようでした
TypeScript では好きにプロパティを追加できなかったので、独自のフィールドを持った GraphQLError を拡張したクラスを作成します

// CustomError.ts
import { GraphQLError } from 'graphql';

type CustomErrorProps = {
  message: string;
  errorType?: string;
};

export class CustomError extends GraphQLError {
  errorType?: string;

  constructor({ message, errorType }: CustomErrorProps) {
    super(message);
    this.errorType = errorType;
  }
}

サーバーの Resolver でエラーを投げている箇所を CustomError を使うように変更します

// server.ts
+ import { CustomError } from './CustomError';

const root = {
  login: ({ username }: { username: string }) => {
    if (username === '') {
-     throw new Error('User Name is required!');
+     throw new CustomError({
+       message: 'User Name is required!',
+       errorType: 'AuthenticationError',
+     });
    }

    return { username };
  },
};

クライアントに返されるエラーに独自フィールドを追加する

express-graphql にはエラーのフォーマットを行う formatError customFormatErrorFn という API が用意されているので、これを使って独自フィールドをエラーに追加します
(※ formatError は express-graphql v 1.0.0 で deprecated になっていました cf. GitHub - graphql/express-graphql: Create a GraphQL HTTP server with Express.)

formatError

function formatError(error: GraphQLError): GraphQLFormattedError

type GraphQLFormattedError = {
  message: string,
  locations: ?Array<GraphQLErrorLocation>
};

type GraphQLErrorLocation = {
  line: number,
  column: number
};

cf. graphql/error | GraphQL

customFormatErrorFn に渡されるエラーオブジェクトは GraphQLError クラスにラップされる

CustomError クラスを throw しているので customFormatErrorFn に渡されるエラーオブジェクトの型を GraphQLError | CustomError な Union 型にすればうまく動作すると思ったのですが、customFormatErrorFn に渡されるエラーオブジェクトは GraphQLError に再度ラップされてしまうようで直接独自フィールドを取得することはできませんでした (ᐡ o̴̶̷̤ ﻌ o̴̶̷̤ ᐡ)

// server.ts
app.use(
  '/graphql',
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
    customFormatErrorFn(error: GraphQLError | CustomError) {
      console.log(error instanceof GraphQLError); // => true
      console.log(error instanceof CustomError); // => false

      return {
        ...error,
        errorType: error?.errorType || undefined,
        // => Unable to compile TypeScript:
        // => Property 'errorType' does not exist on type 'GraphQLError | CustomError'.
        // => Property 'errorType' does not exist on type 'GraphQLError'.
      };
    }
  }),
);

error: GraphQLError | CustomError という Union 型を指定しましたが、error?.errorType と独自フィールドにアクセスしている箇所で次のような TypeScript のコンパイルエラーが発生してしまいます
=> Property 'errorType' does not exist on type 'GraphQLError | CustomError'. Property 'errorType' does not exist on type 'GraphQLError'.

customFormatErrorFn に渡されるエラーオブジェクトは常に error instanceof GraphQLError => true かつ error instanceof CustomError => false です

error.originalError から独自フィールドの値を取り出すようにすればOK

GraphQLErrororiginalError というプロパティに元のエラーオブジェクトを持っています。CustomError クラスを throw した場合は error.originalErrorCustomErrorインスタンスになっているので、ここから独自フィールドを取得するようにすればクライアントに独自のエラー情報を返せます

独自フィールドを取得して返す関数を作成

// CustomError.ts
import { GraphQLError } from 'graphql';

export const getErrorType = (error: GraphQLError): string | null => {
  const errorType = (error.originalError as Record<string, any>)?.errorType;
  if (errorType && typeof errorType === 'string') {
    return errorType;
  }

  return null;
};

独自フィールドを取得してクライアントに返すように修正

// server.ts
- import { CustomError } from './src/CustomError';
+ import { CustomError, getErrorType } from './src/CustomError';

// server.ts
app.use(
  '/graphql',
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
-   customFormatErrorFn(error: GraphQLError | CustomError) {
+   customFormatErrorFn(error: GraphQLError) {
+     const errorType = getErrorType(error);

      return {
        ...error,
-       errorType: error?.errorType || undefined,
+       errorType,
      };
    }
  }),
);

これでサーバーを起動して username を空文字にしたクエリを発行すると独自フィールド errorType を含むエラーが返されるようになりました! Error response has Custom Error Fields!

所感

Express で作った GraphQL サーバーのエラーをカスタマイズできるようになりました!
レベルアップを感じます ₍ ᐢ. ̫ .ᐢ ₎ ₍ ᐢ. ̫ .ᐢ ₎ ₍ ᐢ. ̫ .ᐢ ₎ ヤッタゼ!!

今回も TypeScript の型との戦いでした。
でも JavaScript で書いていたらコンパイルエラーならず動作はしているのに error?.errorType が常に undefined になってしまい何でや!! ともっとハマってしまっていたと思います。ありがとう TypeScript !!


[参考]