はじめに
現在、自社の通販サイトにてPAY.JPを導入しクレジットカード決済機能を利用しています。
開発当初、公式のドキュメントを参考に開発を開始し、
テストでは大きな問題もなく 、導入までスムーズに進みました。
しかし先日、
お客様から「3Dセキュア認証後、画面が進まない」という問い合わせが。
調査を進めると、テストでは見えなかった“思わぬ落とし穴”が見えてきました。
今回は調査の流れから解決方法までを、記事にまとめていきます。
同じようにPAY.JPや3Dセキュア認証を扱う開発者の方の参考になれば幸いです。
3Dセキュアワークフローの補足
PAY.JPの3Dセキュアには、
- iframe型
- リダイレクト型
- サブウィンドウ型
の3種類があります。
その中で、当記事はサブウィンドウ型を採用した事例であり、
挙動を端的に説明すると、
別ウィンドウ(または別タブ)が表示され、そこで認証をする方式となっています。
iframe型の提供前はサブウィンドウ型が主流なワークフローであったため採用しました。
しかし、ポップアップブロックなどにより認証が行えない場合があり、
現在はiframe型が推奨されていますので、新規での導入はお勧めしません。
発端:お問い合わせで気づいた「遷移しない」現象
リリース後、お客様から
「3Dセキュア認証から画面が先に進まない」
という問い合わせをいただきました。
最初はポップアップブロックかカード会社由来のエラーかと思ったものの、
社内で試してみたところ何度目かの決済で同じ現象が再現。
これは何かおかしい……ということで調査を開始しました。
調査で判明:認証に時間がかかると画面が進まなくなる
社内で何度か実カードを使用した決済を行い、決済が成功する場合と失敗する場合を比較。
その結果、3Dセキュアの認証が2分を超えると、
認証成功して元の画面(弊社サイト)に戻っても画面が動かないことに気づきました。
この時、3Dセキュアフローを完了するためのAPI、
POST https://api.pay.jp/v1/charges/:id/tds_finish
は、400 Bad Requestになっていました。
3Dセキュアの認証に時間がかかると、決済画面側のセッションがタイムアウトしていた
多くのカード会社では3Dセキュア認証時に
- 認証コードがメールまたはSMSで届く
- ユーザーが受信を待つ
- 認証画面に戻ってコードを入力
- 認証が完了
という“待ち時間”が必ず発生します。
特にメールの受信にはある程度時間がかかり、
これらの作業に1分〜2分程度の時間がかかることは一般的 です。
しかし、これを考慮できておらず、
- 認証画面を開いた後、親画面では3Dセキュアフロー完了APIをPOST
- まだ認証が終わっておらず400エラー
- 認証成功後画面に戻るも、もうレスポンスを取得できない状態に
- 画面が進まない
という状態になっていたのです。
厳密にはセッションのタイムアウトというより、検証の通り、
POST時にレスポンスが受け取れないことによる400エラーなのですが、
短時間で認証が終わればこれに間に合うことがあるため、この表現をしています。
テスト環境では気づけなかった理由
ローカルや開発用サーバーではPAY.JPもテスト環境で使用しており、
3Dセキュアの認証画面も専用のものが用意されています。
この「PAY.JPの3Dセキュアテストモード」は、
- 認証成功
- 認証失敗
などの結果を即時で選択でき、ワンクリックで認証が完了します。
開発中はこの画面を使い、
数秒で認証→決済完了という流れで繰り返しテストをしており、
POST時にレスポンスが間に合っているため、エラーが発生していませんでした。
しかし、本番環境ではユーザーの操作が加わります。
つまり テスト環境に存在しない「待ち時間」 が本番では必ず発生します。
この差が問題の本質でした。
もちろんリリース直後には、本番で実カードを利用した最終確認もしましたが、
準備万端で3Dセキュアを行ったため、おそらく早めに認証を終えたんだと思います。
その時にもメールの受信に時間がかかっていれば気づけたのに……という悔しさはあります。
解決策:リトライ時間の延長
原因が「3Dセキュア認証途中にセッションが終わること」と分かったため、
決済ページのリトライ時間を延長 する対応を実施しました。
具体的には:
最大100回までtds_finish を再試行、リトライ間隔を3秒とし、
約5分間(100回 × 3秒) は認証結果を待機できるように修正。
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const COMPLETE_TDS_MAX_ATTEMPTS = 100; // 約5分のリトライ猶予
const COMPLETE_TDS_RETRY_DELAY = 3000;
const completeThreeDSecureWithRetry = async (chargeId, attempts = COMPLETE_TDS_MAX_ATTEMPTS, delay = COMPLETE_TDS_RETRY_DELAY) => {
let lastError = null;
for (let i = 0; i < attempts; i += 1) {
try {
const { data } = await store.completeThreeDSecure({ charge_id: chargeId });
return data;
} catch (error) {
lastError = error;
if (i < attempts - 1) {
await wait(delay);
}
}
}
throw lastError;
};
これにより、認証コードの受信待ち〜入力完了までの時間を十分カバーできるようになり、
決済が正常に完了するように改善されました。
5分という時間はやや過剰ではありますが、
本番で実際に決済エラーが発生中の最中の修正であったため、長めに時間をとりました。
まとめ:原因は「認証に時間がかかる」という“本番特有の動作”
今回の問題の本質は、
本番では必ず発生する“認証待ち時間”が、テストでは再現されなかったこと
にありました。
- 本番 → 認証コード受信待ちで待ち時間が1~2分発生
- テスト → 即時認証で待ち時間ゼロ
この差によって、タイムアウト問題が隠れ、リリース後に初めて表面化しました。
タイムアウト時間の延長によって問題は解決しましたが、
3Dセキュアのような「ユーザーが外部アプリ・メールを見る時間」が存在する認証フローでは、
本番想定の待ち時間を考慮した設計の重要性 を改めて感じる出来事でした。
まだ残る不安と改善に向けて
今回の問題は解決したものの、修正対応を進める中で、
- サブウィンドウ型では、ポップアップブロックなどによって認証ができていない可能性があること
- また、ユーザーから問い合わせがあっても、
そういった環境による問題と、障害との切り分けが難しく、調査に余計な手間がかかってしまうこと - 導入した頃と違い、iframe型が登場していること
などの気づきがあり、これを機にワークフロー自体を変更しよう!と決断しました。
次の記事では、“サブウィンドウ型特有の構造的リスク”について取り上げ、
より安定した iframe 型3Dセキュアへ移行した話 を紹介します。
