Cloudflare Workers + StripeでStripeConnectionErrorと謎のエラー地獄から生還した話
はじめに
個人開発でNext.js製のWebアプリをCloudflare Pages/Workersにデプロイし、Stripeを使ったサブスクリプション決済を実装していました。
ローカル開発環境 (localhost:3000) ではすべてが完璧に動作するのに、Cloudflareにデプロイした途端、StripeのAPIを呼び出す全ての処理が、様々な謎のエラーで失敗するという、不可解な問題に長期間悩まされました。
この記事は、その原因を一つずつ突き止め、解決に至るまでの長いデバッグの旅の記録です。
症状:本番環境でだけ次々と現れる謎のエラー
第一の壁:StripeConnectionError
最初に直面したのは、決済ページを作成するAPI (/api/subscription/create-checkout) が500エラーを返し、Cloudflareのログに以下のメッセージが記録される問題でした。
CREATE-CHECKOUT: Failed to create checkout session: Error: An error occurred with our connection to Stripe.
エラーの種類は StripeConnectionError。Stripeとのネットワーク接続自体が失敗していることを示す、最も根本的なエラーでした。
第二の壁:Webhook署名検証エラー
StripeConnectionError を解決した後、次に現れたのはWebhookのエラーでした。テスト決済は完了するものの、データベースが更新されず、Cloudflareのログには以下のメッセージが記録されていました。
Webhook signature verification failed: No signatures found matching the expected signature for payload.
Stripeからの通知は届いているのに、その通知が本物かどうかを検証するセキュリティチェックで失敗していました。
第三の壁:RangeError: Invalid time value
そして、Webhookの問題を解決した後に、最後のボスとして現れたのが、このタイムスタンプエラーでした。
SYNC-LATEST: Error: RangeError: Invalid time value
Webhookが正常に処理されたように見えても、決済完了後に最新情報を同期するAPIが、このエラーで失敗し、UIが正しく更新されませんでした。
犯人探しの長い旅(真犯人の正体)
これらの問題は、すべてStripe公式ライブラリ (stripe-node) と Cloudflare Workersランタイムの非互換性、そしてフレームワークの自動処理という、いくつかの要因が複雑に絡み合った結果でした。
真犯人①:ネットワークAPIの「方言」の違い
-
原因:
stripe-nodeは、Node.js環境を前提に作られているため、ネットワーク通信にhttp/httpsモジュールを使おうとします。しかし、Cloudflare Workersの環境にはそれらが存在せず、代わりにfetchAPIが提供されています。 -
結果:
StripeConnectionErrorが発生していました。
真犯人②:Next.jsの自動おせっかい機能
- 原因: Next.jsは、APIリクエストを受け取ると、開発者が扱いやすいように、自動的にリクエストの中身(JSON)をパース(解析)してくれます。しかし、Stripeの署名検証は、このパースされる前の、生の(raw)リクエスト全体を必要とします。
-
結果:
Webhook signature verification failedエラーが発生していました。
真犯人③:タイムスタンプ形式の罠
-
原因: StripeのAPIが返す時刻データは、秒単位のUNIXタイムスタンプ(例:
1758337294)です。しかし、JavaScriptのnew Date()で日付オブジェクトを正しく作成するには、ミリ秒単位のタイムスタンプが必要です。 -
結果: タイムスタンプの値を1000倍せずに
new Date()に渡してしまったため、RangeError: Invalid time valueが発生していました。
解決策:カスタム実装と正しい作法の実装
これらの問題をすべて解決するため、以下の修正を、Stripeと通信する可能性のある全てのAPIエンドポイントに適用しました。
1. fetchとWeb Crypto APIを使うカスタムプロバイダを実装
StripeライブラリがWorkers環境の「方言」を話せるように、カスタムプロバイダを作成します。
-
StripeFetchHttpClient.ts:fetchAPIを使うカスタムHTTPクライアント -
CustomCryptoProvider.ts:Web Crypto APIを使うカスタムCryptoプロバイダ
// lib/stripe.ts
import Stripe from 'stripe';
import { StripeFetchHttpClient } from './stripe/StripeFetchHttpClient';
import { CustomCryptoProvider } from './stripe/CustomCryptoProvider';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
httpClient: new StripeFetchHttpClient(),
cryptoProvider: new CustomCryptoProvider(),
apiVersion: '2023-10-16',
});
2. Webhookで「生の」リクエストボディを受け取る
Next.jsの自動おせっかい機能を止め、Stripeが必要とする「封筒に入ったままの手紙」を渡してあげます。
// /api/.../webhook/route.ts
import { buffer } from 'micro';
// Next.jsの自動ボディパースを無効化
export const config = {
api: {
bodyParser: false,
},
};
// ... handler関数の中で ...
const buf = await buffer(req); // 生のリクエストボディを取得
const sig = req.headers.get('stripe-signature')!;
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
} catch (err: any) {
// ... エラー処理 ...
}
3. タイムスタンプを正しく変換する
Stripeから受け取った秒単位のタイムスタンプは、必ず1000倍してからnew Date()に渡します。
// ... Webhookやsync-latestの処理の中 ...
const subscription = // ... Stripeから取得したサブスクリプションオブジェクト ...
const startDate = new Date(subscription.current_period_start * 1000);
const endDate = new Date(subscription.current_period_end * 1000);
// ... データベースに保存 ...
後日談:より簡単な解決策の発見
この記事執筆後の発見
記事執筆後に、Stripeが公式でCloudflare Workers対応のHTTPクライアントを提供していることを発見しました。これにより、カスタムプロバイダを実装する必要がなくなります。
// より簡単な実装方法
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
httpClient: Stripe.createFetchHttpClient(), // この1行で解決
apiVersion: '2025-08-27.basil',
timeout: 60000,
maxNetworkRetries: 3,
});
現在の推奨アプローチ
-
ネットワーク互換性:
Stripe.createFetchHttpClient()を使用(カスタム実装不要) -
Webhook処理:
request.text()で生のボディを取得(App Routerの場合) - タイムスタンプ: 引き続き1000倍の変換が必要
まとめ
Cloudflare Workersは非常に強力なプラットフォームですが、Node.jsとは異なる独自の実行環境であることを常に意識する必要があります。
重要なポイント:
-
ネットワーク互換性: カスタムHTTPクライアントの実装、または
Stripe.createFetchHttpClient()の使用 - Webhook処理: 生のリクエストボディを取得し、署名検証に使用
-
タイムスタンプ: Stripeの秒単位タイムスタンプは1000倍してから
new Date()に渡す
この長い戦いの記録が、同じ問題に直面した誰かの助けになれば幸いです。また、技術は常に進歩しており、より簡単な解決策が後から見つかることもあるということも、この体験から学んだ貴重な教訓です。