こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトには公開サイトとは別に、記事や画像を検収するための管理画面があります。管理画面も Next.js のアプリで、こちらは開発サーバではなく本番ビルドで動かしています。毎日使う道具であって、毎日書き換えるコードではないからです。
その管理画面が、スマートフォンから開いたときだけ特定のページで壊れました。原因にたどり着くまで半日かかっています。
症状
iPhone で管理画面を開くと、トップページは普通に表示されます。ところが記事の一覧ページへ移動すると、一瞬だけ中身が見えてから真っ白になり、そのまま 1 秒ほどの間隔でリロードを繰り返します。
Safari が出すメッセージは「このページを開けません」だけです。ネットワークが切れたときと同じ文言なので、通信の問題にしか見えません。
外れた仮説
最初に疑ったのは経路でした。手元では Tailscale を経由してスマートフォンから開発サーバへ繋いでいるので、そこが怪しく思えたのです。
- Tailscale のトンネルが不安定なのではないか
- iOS の Safari が何かをキャッシュしているのではないか
- 端末を再起動すれば直るのではないか
どれも外れました。決定的だったのは、同じ URL を PC のブラウザで開いても同じようにリロードを繰り返したことです。この時点で経路も端末も無関係だと分かりました。
ここまでで半日を溶かしています。「スマートフォンでだけ起きる」と思い込んだのは、たまたま最初に踏んだのがスマートフォンだったからにすぎません。
原因
next start は、起動した時点のビルド成果物を読んで HTML を返します。一方でディスク上の .next/static は、別のプロセスが next build を走らせれば上書きされます。
このとき、変更のあったルートのチャンクはファイル名(ハッシュ)が変わります。つまり古いファイルは消えます。
結果として、次の食い違いが生まれます。
- サーバが返す HTML は、起動時のビルドのチャンク名を指している
- ディスクにあるのは、新しいビルドのチャンクだけ
ブラウザは HTML に書かれた /_next/static/chunks/<hash>.js を取りに行き、それが無いので 500 が返ります。Next.js はチャンクの読み込みに失敗するとページを読み込み直すので、リロードが延々と繰り返されます。
そして「変更のあったルートのチャンクだけ」が消えるので、変更していないトップページは開けます。特定のページだけ壊れる、という症状の形もこれで説明が付きます。
自分の場合、管理画面のコードを直して next build を回した後、動いていたサーバをそのままにしていました。ホットリロードが無いことは分かっていたので「反映されないだけ」だと思っていたのですが、実際には反映されないどころか壊れていたわけです。
見分け方
同じ症状に当たったときに、通信を疑わずに済む手掛かりが 2 つあります。
- トップページは開けるのに、特定のページだけ必ず落ちる。通信の問題ならページによる差は出ません。
- そのページの HTML が指しているチャンクが、ディスクに存在しない。
2 つ目は手で確かめられます。壊れるページのソースを表示して /_next/static/chunks/ のファイル名を控え、ビルド出力の同じディレクトリを ls して探します。そこに無ければ、HTML とディスクが食い違っています。
開発者ツールのネットワークタブでも同じことが見えます。HTML の応答は 200 で、その下のチャンクだけが 500 になります。
対処
直し方はサーバを立て直すだけです。手元では管理コマンドにまとめてあるので、対象を指定して再起動します。
$ npm run dev:restart -- admin
再起動の中でビルドし直すので、HTML とチャンクが揃った状態になります。
そのうえで、この状態を作らないようにしました。管理画面のコードを直したときの手順を「ビルドする」ではなく「再起動する」に統一しています。ビルドだけを単独で回す動機が無くなれば、走行中に成果物を差し替えることもありません。
サーバの一覧を出すコマンドには、各サーバが本番ビルドと開発サーバのどちらで動いているかを列に出すようにしました。ホットリロードが効く前提でいるのか効かない前提でいるのかは、この状態が分かれば取り違えません。
一般化できる部分
これは Next.js 固有の話に見えて、実際には「稼働中のプロセスが読んでいるファイルを、別のプロセスが差し替えた」という一般的な事故です。
同じ形は他でも起きます。
- 稼働中のアプリのアセットを、ビルドで上書きする
- 実行中のスクリプトのファイルを、エディタで保存し直す
- 開いたままのデータベースファイルを、別経路で置き換える
共通しているのは、ファイルの内容が変わったことを稼働中のプロセスが知らない点です。そして、その食い違いはプロセスの中では例外にならず、外から取りに来た誰かが 404 や 500 を受け取る形でしか表に出ません。
だから症状が「通信の問題」に化けます。エラーを出す場所と原因のある場所が離れているので、素直に読むと必ず別のところを疑います。
もうひとつ、教訓として残ったことがあります。「スマートフォンでだけ起きる」という条件を、確かめずに前提として扱ってしまいました。PC で同じ URL を開くのは 1 分の作業で、それを最初にやっていれば半日は使っていません。再現条件を絞る作業は、仮説を立てるより先に置くべきでした。
まとめ
-
next startは起動時のビルドの HTML を返し続けるが、.next/staticは再ビルドで上書きされる - 変更のあったルートのチャンクだけがファイルごと消えるので、そのページだけ 500 になって無限リロードする
- ブラウザは「ページを開けません」としか言わないので、通信や端末の問題に見える
- 見分け方は「トップは開けるのに特定のページだけ落ちる」+「HTML が指すチャンクがディスクに無い」
- 直すのは再起動。作らないためには、ビルド単独ではなく再起動を手順にする
- 再現条件(端末・経路)は思い込む前に 1 つずつ外して確かめる