webMethod の第2引数を関数リテラルにしなかっただけで、バックエンドの API が全部消えた
- 5 時間前
- 読了時間: 5分
管理者向けのダッシュボードで対象日を選んでも、一覧が出ない。画面のステータス欄に出たのは一行だけだった。
(0 , t.getSummary) is not a functionこの一行で最初に疑ったのは、サイト上のファイルが古いことだった。Wix エディタへ手で貼り付けて運用しているので、ローカルの最新版が届いていないことは実際に何度もある。貼り直して、公開して、もう一度押した。同じ一行が出た。二度目も同じだった。
このエラーが何を指しているか
t は、フロントエンドが受け取る backend/summaryOps.web.js のクライアントプロキシである。Velo は .web.js をフロントエンドから import できるように、呼び出しを中継するオブジェクトをビルド時に作る。それが t になっている。
読み方は素直で、t 自体は存在するのに getSummary だけが無い。つまりモジュールは解決されていて、その中身が空だということになる。実際、このファイルは5つの関数を export していて、5つとも同じように消えていた。一部ではなく全滅である。
ページ側は動いていた。画面の再読み込み処理が走ってステータス欄に文字が出ているのだから、ダッシュボードのページコードには罪がない。壊れているのはサイト上のバックエンドだ、というところまでは早かった。
ローカルに原因が無いことを先に確定させる
貼り直しても直らないのだから、次は「ローカルのコードが本当に正しいのか」を疑う番になる。ここは機械的に潰した。
検査 | 結果 |
関係する6ファイルの named import を、実際の export と突き合わせる | 欠けている名前は0件 |
同6ファイルの ESM 構文チェック(.mjs にコピーして node --check) | すべて ok |
backend/jobs.config の関数の場所と名前 | 実ファイルの export と一致 |
全部通った。ローカルが正しくて、貼り直しても直らない。ここで手が止まった。
動いている .web.js と形を突き合わせる
同じサイトには、実機で動いている .web.js が4つあった。会員プロフィールを扱うもの、マスタを読むもの、注文をキャンセルするもの、テスト用の注文を作るものである。どれもフロントエンドから普通に呼べている。壊れているファイルだけが持っている性質があるはずだと考えて、5つを並べた。
その .web.js の役割 | export 数 | webMethod() の第2引数 | 実機 |
会員プロフィールの読み書き | 5 | async (…) => {…} | 動いている |
マスタの取得 | 5 | async (…) => {…} | 動いている |
注文のキャンセル | 3 | async (…) => {…} | 動いている |
テスト用の注文の作成 | 4 | async (…) => {…} | 動いている |
集計(症状が出ていたもの) | 5 | guard(…) の戻り値 | 空 |
動いている14個の export は、例外なく関数リテラルを直接渡していた。壊れているファイルだけが、ヘルパ関数の戻り値を渡していた。
export const getSummary = webMethod(
Permissions.Admin,
guard('getSummary', runGetSummary, { … }) // ヘルパの戻り値
);guard() は非同期関数を返す。構文として正しく、実行時にも第2引数には関数が入る。JavaScript としては何も間違っていない。
ビルド時に読まれる前提で書く
それでも動かない理由は、この判定が実行時ではなくビルド時に行われることにある。Velo は .web.js を静的解析してクライアント側のプロキシを生成する。第2引数が関数リテラルでなければ webMethod として認識されず、プロキシが空になる。モジュールそのものは解決されるので、フロントエンドから見ると「オブジェクトはあるのに関数が1つも生えていない」という形になる。
症状の出方には、そのまま4つの特徴が現れていた。
画面に出るのは (0 , t.getSummary) is not a function だけで、原因を示す情報が何も無い
そのファイルの export が全滅する。一部だけ欠けることはない
エディタにビルドエラーが出ない。ファイルは構文的に正しいので、止まる理由が無い
ページコードは正常に動く。だから「ページは無罪」という正しい切り分けが、そのまま行き止まりになる
この形にしたのは実装した日からで、つまりこの画面は一度も動いたことがなかった。「前は動いていたのに」という前提が最初から成立していなかったわけで、サイト上のファイルが古いという仮説に二往復を使ったのは、そこを確かめていなかったからだった。
共通処理は捨てずに、リテラルの中から呼ぶ
guard() でやっていたことには意味がある。どの関数で落ちたかを戻り値に載せ、依存が届いていなければ名前を挙げて返す、という診断の共通処理である。これを捨てる必要はない。呼ぶ位置を関数リテラルの内側へ移すだけでよい。
async function runGuarded(label, getFn, fallback, input) { … } // 素のヘルパ
export const getSummary = webMethod(
Permissions.Admin,
async (input) => runGuarded('getSummary', () => runGetSummary, FALLBACK.summary, input)
);この形なら静的解析は第2引数を関数リテラルとして読み、プロキシに getSummary が生える。共通処理はその内側で普通に呼ばれる。貼り直して公開したところ、症状は is not a function から別のエラーへ変わった。関数が無いエラーから、関数が実行されて返らないエラーへの変化なので、ここは解消したと判断できる。
同じ症状を次に見たとき
「モジュールは届くのに中身が0個」という形は、ローカルの静的検査では出てこない。import と export の突き合わせも、構文チェックも、この障害には反応しない。判定しているのがビルド時の Velo だからで、手元でいくら正しさを確認しても、確認している対象が違う。
そのときいちばん速いのは、同じサイトで動いている .web.js と形を突き合わせることだった。動いている実例が同居しているのは、Velo の案件ではむしろ普通のことだと思う。差分は1か所しかなかった。
.web.js の webMethod() は、第2引数を必ず関数リテラルで書く。共通処理を挟みたくなったら、ラッパで包むのではなく、リテラルの中で呼ぶ。
コメント