Check! LINEボット用 Webhook を TypeScript 対応フレームワーク Nest で作る（トラブルシューティング）

こんにちは、 @dz_ こと大平かづみです。

# Prologue - はじめに

LINEボットをつくる機会があり、試作を始めました。

LINEボットを作成するには Messaging API を利用します。

- [概要 - Messaging API](https://developers.line.biz/ja/docs/messaging-api/overview/)

今回は、[Node.js SDK for Messaging API](https://line.github.io/line-bot-sdk-nodejs/) （以降、 LINE SDK と記述）を利用します。

Node.js SDK for Messaging API は、[Express](http://expressjs.com/) をベースとしています。Express はよく使われており小規模の実装ならちょうどよいのですが、今回はボットの応答だけでなく、ほかのAPIも実装したいので、ちょっと大変になりそうです。そこで、フレームワークには TypeScript をサポートしている [Nest](https://nestjs.com/) を選択しました。

[TypeScript](https://www.typescriptlang.org/) は、「型」の概念を組み込んだ Node.js のスーパーセットです。型定義があることにより、コードを書く時の迷いが少なく、また、JavaScript へコンパイルするタイミングで構文エラーがわかるので品質も向上します。登場してから時間も経ち、TypeScript をサポートするフレームワーク、ライブラリも増えてきました。Node.js SDK for Messaging API も TypeScript をサポートしています。

- [TypeScript \| line-bot-sdk-nodejs](https://line.github.io/line-bot-sdk-nodejs/guide/typescript.html)

さて、Nest も Express をベースとしているので、これはすぐつながると高をくくっていたのですが、ハマったのでメモを残します。

なお、ここでは Nest のフレームワークや LINE SDK の詳しい解説はしません。

# ハマったところ: シグネチャ解析に失敗する

## エラーの解説
Nest はベースとなるプラットフォームを選択でき、デフォルトでは Express が設定されていて、 Express 用の middleware も利用できます。

- [Platform - Documentation \| NestJS - A progressive Node.js web framework](https://docs.nestjs.com/first-steps#platform)
- [middleware - Documentation \| NestJS - A progressive Node.js web framework](https://docs.nestjs.com/middleware)

詳細は省きますが、LINE SDK の middleware を指定するにはこんな感じです。 _LINE_CHANNEL_ACCESS_TOKEN_ と _LINE_CHANNEL_SECRET_ については適宜読み替えてください。

```ts:app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LineWebhookController } from './line-webhook/line-webhook.controller';  // Webhook 用の自作コントローラ
import { middleware as LineMiddleware, MiddlewareConfig } from '@line/bot-sdk'; // LINE SDK

@Module({
  imports: [],
  controllers: [AppController, LineWebhookController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
  
    // config にアクセストークンとシークレットを設定する
    const lineConfig: MiddlewareConfig = {
      channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
      channelSecret: process.env.LINE_CHANNEL_SECRET,
    };
    // config を指定して LINE SDK の middleware を指定する
    consumer
      .apply(LineMiddleware(lineConfig))
      .forRoutes(LineWebhookController);
  }
}

```

ですが、これだけだと LINE から Webhook が呼ばれたときに、以下のエラーが出てしまいます。

```
(node:3996) UnhandledPromiseRejectionWarning: TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be one of type string, TypedArray, or DataView. Received type object
    at Hmac.update (internal/crypto/hash.js:58:11)
    at Object.validateSignature [as default] (\api\node_modules\@line\bot-sdk\dist\validate-signature.js:37:10)
    at getBody.then.body (\api\node_modules\@line\bot-sdk\dist\middleware.js:37:46)
    at process._tickCallback (internal/process/next_tick.js:68:7)
```

これは、LINE SDK middleware でシグネチャ解析時に、オブジェクト化されていない（bodyParserでパースされていない）状態の body を使いたいが、Nest の Express プラットフォームが先に bodyParser を呼んでしまい、body がオブジェクト化されてしまうためです。

下記ドキュメントにも、 bodyParser より前に LINE SDK middleware を指定するよう注意書きがあります。

> Do not use another body-parser before the webhook middleware()

- [Webhook \| line-bot-sdk-nodejs](https://line.github.io/line-bot-sdk-nodejs/guide/webhook.html#build-a-webhook-server-with-express)


普段はオブジェクト化されてる方が便利ですが、LINE SDK のシグネチャ突合ができないのは困りものです。

なお、Nest の Express プラットフォームで bodyParser が設定されるところはこちらです。

- [nest/express-adapter.ts at 205d73721402fb508ce63d7f71bc2a5584a2f4b6 · nestjs/nest · GitHub](https://github.com/nestjs/nest/blob/205d73721402fb508ce63d7f71bc2a5584a2f4b6/packages/platform-express/adapters/express-adapter.ts#L124-L132)

## 解決策

そこで、以下のように対応しました。

- `main.ts` で、デフォルトの bodyParser を無効にする
- `app.module.ts` で、先にLINE SDK middleware を適用し、後に bodyParser を適用する

```ts:main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';

async function bootstrap() {
  // NestApplicationOptins の bodyParser の設定を無効にする
  const options: NestApplicationOptions = {
    bodyParser: false,
  };
  // AppModule生成時に options を指定する
  const app = await NestFactory.create(AppModule, options);
  await app.listen(3000);
}
bootstrap();

```

```ts:ap.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LineWebhookController } from './line-webhook/line-webhook.controller';  // Webhook 用の自作コントローラ
import { middleware as LineMiddleware, MiddlewareConfig } from '@line/bot-sdk';  // LINE SDK
import bodyParser = require('body-parser');  //  bodyParser

@Module({
  imports: [],
  controllers: [AppController, LineWebhookController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
  
    // config にアクセストークンとシークレットを設定する
    const lineConfig: MiddlewareConfig = {
      channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
      channelSecret: process.env.LINE_CHANNEL_SECRET,
    };
    // config を指定して LINE SDK の middleware を指定する
    consumer
      .apply(LineMiddleware(lineConfig))
      .forRoutes(LineWebhookController);

    // その後、bodyParser を適用する
    consumer
      .apply(bodyParser.json(), bodyParser.urlencoded({ extended: false }));
  }
}

```

できましたー！ :raised_hands:

# Epilogue - おわりに

これで、 Nest フレームワークと LINE SDK を組み合わせて使えるようになりました。

実装に進むぞ～！