「予約に失敗しました。時間をおいて再度お試しください。」
このエラーメッセージが表示されていると報告が来た。予約フォームから送信すると、確定画面に進まずエラーになる。
実際に試した。エラーが再現した。
エラーの連鎖を追いかけた
フロントエンドの submitBooking() が fetch('/api/register') を呼ぶ。EC2のFastAPIが受け取り、GASに転送する。GASが処理してJSONを返す。FastAPIがそれをフロントエンドに返す。
エラーはフロントエンドの d.success が undefined のときに表示される判定があった。
d.success がなぜ undefined になるのかを追った。
FastAPIのログを確認すると r.json() の呼び出しで例外が発生していた。GASが返したレスポンスがJSONではなく、HTMLだった。
GASがHTMLエラーページを返していた。
GASが例外を投げていた
GASのコードを確認した。pushMessage() 関数の中でLINE APIを呼んでいた。
function pushMessage(userId, messages) {
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', {
method: 'post',
contentType: 'application/json',
headers: { 'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN },
payload: JSON.stringify({ to: userId, messages: messages })
});
}
muteHttpExceptions の指定がなかった。
GASの UrlFetchApp.fetch() はデフォルトで、レスポンスのHTTPステータスが4xx・5xxの場合に例外をスローする。LINE APIが何らかの理由でエラーを返すと、GASが例外を投げる。
doGet 全体に try/catch もなかった。例外が捕捉されず、GASはHTMLのエラーページをレスポンスとして返す。FastAPIがそれをJSONとして解析しようとして失敗する。エラーオブジェクトが返る。フロントエンドが「失敗しました」を表示する。
修正は1行
function pushMessage(userId, messages) {
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', {
method: 'post',
contentType: 'application/json',
headers: { 'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN },
payload: JSON.stringify({ to: userId, messages: messages }),
muteHttpExceptions: true
});
}
muteHttpExceptions: true を追加した。これでLINE APIが4xx・5xxを返しても例外はスローされない。ステータスコードを無視して処理を続ける。
replyMessage() も同じ構造だったので同様に修正した。
doGetにtry/catchを追加した
muteHttpExceptions で例外の発生源は塞いだ。それとは別に、doGet 全体を try/catch で囲んだ。
どこかで予期しない例外が発生してもHTMLエラーページを返さない。代わりにJSONで {success: false, error: "..."} を返す。FastAPIが正常に解析できる。フロントエンドが適切なエラーメッセージを表示できる。
エラーの連鎖を一箇所で断ち切る設計だ。
予約は通っていた
調査の過程で気づいたことがある。「失敗しました」と表示されていたにもかかわらず、スプレッドシートには予約が記録されていた。
GASが例外を投げたのはLINE通知を送る段階だった。予約の記録自体は完了していた。通知が失敗したことでGASが落ち、フロントエンドにエラーが伝わった。
ユーザーは「予約できなかった」と思いながら、実際には予約が入っていた。混乱のもとになる状態だった。通知の失敗が予約の失敗に見えてしまう設計の問題だった。
*シンプルシステム株式会社 代表 伊藤勝彦*