自作プラグインで管理バーにショートカットリンクを追加する改修を入れたところ、デプロイ直後からログイン中のユーザーだけ、フロントのどのページを開いても「このサイトで重大なエラーが発生しました。」になりました。ログインしていない訪問者には何も起きないため、外形監視は正常のまま。原因は、管理画面でしか読み込まれないクラスをフロントでも動くフックから参照していたことでした。この記事では実際のエラーログから原因と修正、そして「なぜ監視で気づけないか」までを残します。
起きたこと ― ログインしている人にだけサイト全体が死ぬ
入れた改修は小さなものです。管理バー(画面上部の黒いバー)に、プラグインの管理ページへ1クリックで飛べるリンクを2つ追加しただけ。デプロイして動作確認し、問題なしと判断しました。
ところが直後に「このサイトで重大なエラーが発生しました、と出ている」と報告が来ます。慌てて確認すると、こちらのブラウザでも再現する。しかしcurlで叩くとトップも記事ページも200を返している。PHP-FPMのエラーログを見て原因が分かりました。
PHP Fatal error: Uncaught Error: Class "Myplugin_Admin" not found
in /var/www/html/wp-content/plugins/myplugin/includes/class-myplugin-toolbar.php:36
(クラス名・パスは一般化しています。構造は実ログのままです)
原因 ― is_admin()のときだけrequireされるクラスをフロントで参照した
このプラグインは、管理画面専用のクラスを次のように読み込んでいました。管理画面でしか使わないコードをフロントのリクエストに載せない、それ自体はよくある真っ当な作りです。
if ( is_admin() ) {
require_once MYPLUGIN_DIR . 'includes/class-myplugin-admin.php';
}
一方、今回追加した管理バーのリンクは admin_bar_menu フックで動きます。そして権限チェックに、管理画面クラスが持つ定数を使ってしまいました。
public static function add_shortcuts( $bar ) {
if ( ! current_user_can( Myplugin_Admin::CAPABILITY ) ) { // ← ここ
return;
}
// リンクを追加…
}
見落としていたのは、admin_bar_menu は管理画面だけでなくフロントでも発火するという点です。管理バーはログイン中ならフロントにも表示されるので当然の仕様ですが、「管理バー=管理画面のもの」という思い込みがありました。フロントのリクエストでは is_admin() がfalseなので Myplugin_Admin はrequireされておらず、定数参照の行で即Fatal。管理バーはbodyの描画中に組み立てられるため、ページ全体が「重大なエラー」画面になります。
修正 ― クラス定数をやめて権限文字列を直書きする
修正は1行です。ロード状況に依存するクラス定数の参照をやめ、権限文字列を直接書きました。
public static function add_shortcuts( $bar ) {
// 管理画面クラスは is_admin() のときしか読み込まれないため、
// フロントでも動くこのフックからクラス定数を参照してはいけない(実際にFatalになった)。
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// リンクを追加…
}
「定数の重複管理になるのでは」という指摘はその通りで、きれいにやるなら定数を管理画面クラスではなく、常に読み込まれる本体ファイルへ移すのが筋です。今回は障害対応としてまず直書きで止血し、コメントに理由を残しました。class_exists() でガードする手もありますが、権限チェックが目的なら文字列で十分です。
なぜ外形監視で気づけないのか
この障害の嫌らしさは検知の難しさにあります。Fatalの発生条件が「管理バーが表示されること」つまりログインしていることなので、非ログインでHTTPステータスを見る死活監視やuptime系のサービスはずっと200を返し続けます。実際、障害発生中にcurlで確認したトップページ・記事ページはすべて200でした。
異変に気づけるのは、ログインした状態でフロントを見た人間だけ。今回は運良く直後に報告があったので数分で復旧できましたが、報告がなければ「管理者がたまたまフロントを見るまで」放置されていたはずです。
再発防止 ― フロントで動くフックを触ったらログイン状態で確認する
デプロイ後の確認として、次を手順に加えました。
admin_bar_menu・wp_footer・initなどフロントでも発火するフックを触った変更は、ログインした状態でフロントページを開いて確認する。curlやwp evalでの確認はこの障害を再現できないis_admin()でガードしてrequireしているクラス・関数を、ガードの外から参照していないかを変更時にgrepする- PHPのエラーログの場所(この環境ではPHP-FPMのログ)を事前に把握しておく。「重大なエラー」画面自体は原因を何も教えてくれないので、ログに一直線で行けるかが復旧時間を決める
「管理バーにリンクを足すだけ」の改修でも、動く場所が管理画面とフロントの両方にまたがるコードは、両方で確認する。当たり前の教訓ですが、実際にサイト全体を落としてから身に沁みました。