AWS AppSync+Lambda Resolverでエラー処理

2022.05.10

吉川です。

早速ですが、AppSync+Lambdaリゾルバにおいて、(VTLではなく)Lambda関数からAppSync経由でクライアントへ errors を返すにはどのようなコードを書けば良いのかについて調べたので共有します。

結論から言うと、 callback('Error Message.') もしくは throw new Error('Error Message.') どちらかのパターンを使うと良さそうでした。

環境

  • node 16.13.0
  • typescript 3.9.7
  • aws-cdk-lib 2.20.0
  • @aws-cdk/aws-appsync-alpha 2.20.0-alpha.0
  • constructs 10.0.115

前提

CDKコード

lib/appsync-stack.ts

import { Construct } from 'constructs'
import * as cdk from 'aws-cdk-lib'
import * as appsync from '@aws-cdk/aws-appsync-alpha'
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'

export class AppSyncStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    /**
     * Backend
     */
    // AppSync API
    const api = new appsync.GraphqlApi(this, 'myAppsyncApi', {
      name: 'myAppsyncApi',
      schema: appsync.Schema.fromAsset('./schema.graphql'),
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: appsync.AuthorizationType.API_KEY,
        },
      },
    })

    // Lambda関数
    const userFn = new lambdaNodejs.NodejsFunction(this, 'userFn', {
      entry: 'lambda-handler/find-user-handler.ts',
    })

    // Lambda関数をDataSourceとしてAppSyncAPIと紐付ける
    const userDs = api.addLambdaDataSource('userDs', userFn)

    // schema.graphqlで定義した中のどの操作とマッピングするかを指定
    userDs.createResolver({
      typeName: 'Query',
      fieldName: 'user',
    })
  }
}

スキーマファイル

schema.gql

type User {
  id: String!
  name: String!
}
type Query {
  user: User!
}

投げるクエリ

以下のクエリをAppSyncマネジメントコンソールで投げて検証します。

query MyQuery {
  user {
    id
    name
  }
}

Lambda Resolverのエラーハンドリングパターンを色々試してみる

では、確認したパターンを紹介したいと思います。

callback()に文字列を渡す

callback() 関数にメッセージを渡すことでクライアントにエラーを返すことができます。

find-user-handler.ts

import { AppSyncResolverEvent, Callback, Context } from 'aws-lambda'

export const handler = async (
  event: AppSyncResolverEvent<{}, {}>,
  _: Context,
  callback: Callback
) => {
  console.log(JSON.stringify({ event }))

  callback('Example error message.')
}

レスポンス

{
  "data": null,
  "errors": [
    {
      "path": [
        "user"
      ],
      "data": null,
      "errorType": "string",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Example error message."
    }
  ]
}

Errorをthrowする

Errorthrow することでクライアントにエラーを返すことができます。

find-user-handler.ts

import { AppSyncResolverEvent, Callback, Context } from 'aws-lambda'

export const handler = async (event: AppSyncResolverEvent<{}, {}>) => {
  console.log(JSON.stringify({ event }))

  throw new Error('Example error message.')
}

レスポンス

{
  "data": null,
  "errors": [
    {
      "path": [
        "user"
      ],
      "data": null,
      "errorType": "Error",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Example error message."
    }
  ]
}

callback()にErrorを渡す

callback() 関数に Error を渡すことでもクライアントにエラーを返すことができます。

import { AppSyncResolverEvent, Callback, Context } from 'aws-lambda'

class CustomError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'CustomError'
  }
}

export const handler = async (
  event: AppSyncResolverEvent<{}, {}>,
  _: Context,
  callback: Callback
) => {
  console.log(JSON.stringify({ event }))

  callback(new Error('Example error message.'))
}

レスポンス

{
  "data": null,
  "errors": [
    {
      "path": [
        "user"
      ],
      "data": null,
      "errorType": "Error",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Example error message."
    }
  ]
}

カスタムErrorをthrowする

Error を継承したカスタム Errorthrow でエラーを返せます。

find-user-handler.ts

import { AppSyncResolverEvent, Callback, Context } from 'aws-lambda'

class CustomError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'CustomError'
  }
}

export const handler = async (event: AppSyncResolverEvent<{}, {}>) => {
  console.log(JSON.stringify({ event }))

  throw new CustomError('Custom error message.')
}

レスポンス

{
  "data": null,
  "errors": [
    {
      "path": [
        "user"
      ],
      "data": null,
      "errorType": "CustomError",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Custom error message."
    }
  ]
}

this.name = 'CustomError' を記述しないと errorType はデフォルトの Error のままだったのでご注意ください。

なお、割愛しますが「 callback() にカスタムErrorを渡す」パターンも同じように使うことができました。

[NG] 文字列をreturnする

callback()に文字列を渡すパターン に準ずるかと思いましたが、こちらはできないようでした。

find-user-handler.ts

import { AppSyncResolverEvent, Callback, Context } from 'aws-lambda'

export const handler = async (event: AppSyncResolverEvent<{}, {}>) => {
  console.log(JSON.stringify({ event }))

  return 'Example error message.'
}

レスポンス

{
  "data": null,
  "errors": [
    {
      "path": [
        "user",
        "id"
      ],
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'String' within parent 'User' (/user/id)"
    },
    {
      "path": [
        "user",
        "name"
      ],
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'String' within parent 'User' (/user/name)"
    }
  ]
}

[NG] Errorをreturnする

こちらも callback()にErrorを渡すパターン に準ずるかと思いましたが、できないようでした。

find-user-handler.ts

import { AppSyncResolverEvent, Callback, Context } from 'aws-lambda'

export const handler = async (event: AppSyncResolverEvent<{}, {}>) => {
  console.log(JSON.stringify({ event }))

  return new Error('Example error message.')
}

レスポンス

{
  "data": null,
  "errors": [
    {
      "path": [
        "user",
        "id"
      ],
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'String' within parent 'User' (/user/id)"
    },
    {
      "path": [
        "user",
        "name"
      ],
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'String' within parent 'User' (/user/name)"
    }
  ]
}

以上、参考になれば幸いです。

参考