8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare Workersで、Stripe Webhookから送信されるイベントを処理するAPIを作成する方法

Last updated at Posted at 2023-08-28

Cloudflare WorkersはV8ベースのランタイムで動作しています。

そのため、npmに公開されているライブラリの中には、Cloudflare Workersで動作しないものも存在します。

stripe-nodeはCloudflare Workersでも利用可能

StripeのNode.js向けラリブラリであるstripe-nodeは、2023年4月にCloudflare Workersへの対応を完了しました。

Stripe Webhookを処理するAPIも、Cloudflare Workersを利用して構築できます。

この記事では、Cloudflare WorkersでStripeのWebhookイベントを検証する方法を紹介します。

HonoでAPIを作成する

Cloudflare WorkersでAPIを作成するには、Honoを利用すると便利です。

npm createでアプリをセットアップします。

$ npm create hono hono-cfw

どのサービスにデプロイするかを尋ねられます。

cloudflare-workersを選びましょう。

? Which template do you want to use? › - Use arrow-keys. Return to submit.
    aws-lambda
    bun
    cloudflare-pages
❯   cloudflare-workers
    deno
    fastly
    lagon
    lambda-edge
    netlify
  ↓ nextjs

テンプレートからファイルが生成されます。

ディレクトリに移動して、ライブラリをインストールしましょう。

% cd hono-cfw
% npm install

最後にWranglerでローカル環境にてAPIを立ち上げましょう。

% npm run dev
warning package.json: No license field
$ wrangler dev src/index.ts
 ⛅️ wrangler 3.5.1
------------------
wrangler dev now uses local mode by default, powered by 🔥 Miniflare and 👷 workerd.
To run an edge preview session for your Worker, use wrangler dev --remote
⎔ Starting local server...
[mf:inf] Ready on http://127.0.0.1:65373/

cURLでAPIが動作することを確認しましょう。

% curl http://127.0.0.1:65373/
Hello Hono

Honoで、Webhook用のAPIを追加する

Webhook用のパスを追加しましょう。

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))
+app.post('/webhook', async (c) => {
+    console.log(await c.req.json())
+    return c.json({
+        message: 'ok'
+    })
+})

export default app

これで/webhookへのPOSTリクエストを受け付けることができます。

Stripe CLIでStripe WebhookをローカルのAPIに中継する

Stripeで発生したイベントをローカルのAPIに中継するには、Stripe CLIを使います。

% stripe listen --forward-to http://127.0.0.1:65373/webhook

--forward-toのURLは {npm run devで表示されたURL}/webhook を指定しましょう。

Webhookのシークレットが発行されますので、コピーしましょう。

Your webhook signing secret is whsec_xxxxx (^C to quit)

環境変数は、.dev.varsファイルへ

Wranglerを使って開発する場合、ローカルで利用する環境変数は.dev.varsに保存します。

先ほどコピーしたシークレットも、.dev.varsに保存しましょう。

.dev.vars

STRIPE_WEBHOOK_SECRET=whsec_xxxxx

あわせて、Stripeのシークレットキーも保存しておきましょう。
ダッシュボード直リンク: https://dashboard.stripe.com/test/apikeys

.dev.vars

STRIPE_WEBHOOK_SECRET=whsec_xxxxx
STRIPE_SECRET_KEY=sk_test_xxxx

アプリを再起動して、環境変数を読み込み直す

環境変数を設定しましたので、npm run devを実行し直しましょう。

% npm run dev
warning package.json: No license field
$ wrangler dev src/index.ts
 ⛅️ wrangler 3.5.1
------------------
wrangler dev now uses local mode by default, powered by 🔥 Miniflare and 👷 workerd.
To run an edge preview session for your Worker, use wrangler dev --remote
⎔ Starting local server...
[mf:inf] Ready on http://127.0.0.1:123456/

APIのURL(ポート番号)が変わりますので、stripe listen --forward-toも再度実行します。

Webhookのシークレットは、stripe listenを再起動しても変更されません。

% stripe listen --forward-to http://127.0.0.1:123456/webhook

「Stripeからのリクエストか」を検証する処理を追加する

作成したWebhook APIを、Stripe Webhook以外から呼び出されないようにしましょう。

Stripeの場合、Stripe SDKを利用して検証処理を追加します。

% npm i stripe

続いて作成したAPIパスに、検証処理を追加しましょう。

import { Hono } from 'hono'
import { env } from 'hono/adapter'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))
app.post('/webhook', async (c) => {
    const { STRIPE_SECRET_API_KEY, STRIPE_WEBHOOK_SECRET } = env(c)
    const stripe = new Stripe(STRIPE_SECRET_KEY, {
        apiVersion: "2023-08-16",
    });
    const signature = c.req.headers.get("stripe-signature");
    try {
        if (!signature) {
            return c.json({
                message: 'Bad Request'
            }, 400)
        }
        const body = await c.req.text();
        const event = await stripe.webhooks.constructEventAsync(
            body,
            signature,
            STRIPE_WEBHOOK_SECRET
        );
        switch(event.type) {
            case 'payment_intent.created': {
                console.log(event.data.object)
                break
            }
            default:
                break
        }
        return c.json({
            message: 'ok'
        }, 200)
      } catch (err) {
        const errorMessage = `⚠️  Webhook signature verification failed. ${err instanceof Error ? err.message : 'Internal server error'}`
        console.log(errorMessage);
        return c.json({
            message: errorMessage
        }, 400)
      }
})

export default app

Cloudflare WorkersでStripe Webhookを処理する際のポイント

Webhook APIを開発されたことがある方は、Next.jsやAWS Lambda / firebaseなどで処理する際と実装が少し異なることに気づかれるかもしれません。

ドキュメントで掲載している実装と、Cloudflare Workersで動かすための実装の差分を以下にDiffとして紹介します。

https://stripe.com/docs/webhooks/quickstart

    const stripe = new Stripe(STRIPE_SECRET_KEY, {
        apiVersion: "2023-08-16",
    });
    const signature = c.req.headers.get("stripe-signature");
    try {
        if (!signature) {
            return c.json({
                message: 'Bad Request'
            }, 400)
        }
        const body = await c.req.text();
-        const event = stripe.webhooks.constructEvent(
-            request.body,
-            signature,
-            endpointSecret
-        );
+        const event = await stripe.webhooks.constructEventAsync(
+            body,
+            signature,
+            STRIPE_WEBHOOK_SECRET
+        );
        switch(event.type) {
            case 'payment_intent.created': {
                console.log(event.data.object)
                break
            }
            default:
                break
        }
        return c.json({
            message: 'ok'
        }, 200)
      } catch (err) {
        const errorMessage = `⚠️  Webhook signature verification failed. ${err instanceof Error ? err.message : 'Internal server error'}`
        console.log(errorMessage);
        return c.json({
            message: errorMessage
        }, 400)
      }
})

export default app

もし「Cloudflare Workers / Pagesでは、Stripe Webhookがうまく動かない・・・」というケースに遭遇された場合は、この実装方法の違いに対応できているかをご確認ください。

Stripe CLIでWebhookイベントをテストする

実際に検証やWebhookのリクエストが処理できているかを確認しましょう。

curlで直接APIを呼び出すと、エラーが出ます。

% curl -XPOST http://127.0.0.1:51351/webhook
{"message":"Bad Request"}

Stripe CLIでイベントをシミュレートしましょう。

% stripe trigger payment_intent.created
Setting up fixture for: payment_intent
Running fixture for: payment_intent
Trigger succeeded! Check dashboard for event details.

npm run devを実行している画面にPayment Intentの情報が表示されます。

[mf:inf] Updated and ready on http://127.0.0.1:51351/
Object {
  id: pi_3NhnLTL6R1kGwUF41dAgicMq,
  object: payment_intent,
  amount: 2000,
  amount_capturable: 0,
  amount_details: Object
  ...
}

StripeとCloudflareを利用したアプリを開発する際のリソース

StripeとCloudflareを組み合わせた例については、Zennにワークショップ記事をいくつか公開しています。

こちらも併せてお試しください。

More resource

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?