公開
CDN 向けの s-maxage がブラウザに届いていた — デプロイ後に消えたチャンクを読みに行く
Vercel が書き換えていた Cache-Control が Workers ではそのままブラウザに届き、デプロイ後に古い RSC が消えたチャンクを読みに行って画面が崩れた。Worker のラッパーで書き換え、Next.js の 8 本に同じ修正を入れた。
Vercel から Cloudflare Workers へ移したあとも、しばらく残っていた差の話です。プラットフォームが黙って書き換えていたヘッダーは、移行しても設定に現れないので見落としやすいものでした。
症状
デプロイの直後にページを開くと、もう存在しない /_next/static/chunks/*.css|*.js を読みに行って 404 になり、コンソールに次のようなエラーが出ました(2026-10-20 に resume の本番で確認)。
Refused to apply style ... MIME type ('text/plain')
Refused to execute script ...
スタイルが当たらなくなるほか、クライアント側の画面遷移も同じ理由で壊れることがあります。
原因
Next.js は事前生成したページと、その RSC ペイロード(ルーターが先読みと遷移で取りに行く ?_rsc=)に次のヘッダーを付けます。
cache-control: s-maxage=31536000, stale-while-revalidate=2592000
これは CDN 向けの値です。Vercel はブラウザへ渡す前に public, max-age=0, must-revalidate へ書き換えますが、Workers ではそのまま届きます。
ブラウザは s-maxage を無視する一方で stale-while-revalidate には従うので、RSC ペイロードを最大 30 日ディスクキャッシュから返し、裏で再検証します。CDP で測ると、2 回目の読み込みは fromDiskCache: true でした。デプロイ後はその古いペイロードが旧ビルドのチャンク名を指しているため、404 になります。
housing-performance-simulator のように s-maxage だけが付く場合にも別の問題があります。途中に共有キャッシュがあると、毎リクエストの nonce が入った HTML を 1 年保持できてしまいます。
直し方
Worker のラッパーに小さな純関数を足しました。
browserCacheControl(value):s-maxageディレクティブを含む値だけをpublic, max-age=0, must-revalidateにする。判定はディレクティブ名として見る(/(^|,)\s*s-maxage\s*=/i)ので、名前がs-maxageで終わる別のディレクティブには反応しないapplyBrowserCacheControl(response): 値が変わるときだけ応答を作り直す。fetch()由来の応答はヘッダーが書き換えられないので、元の応答には触らない。Cache-Control の無い応答には足さない
これを OpenNext が返すすべての応答(HTML、text/x-component の RSC、route handler、リダイレクト)に、nonce の処理の前に通します。HTML だけでは足りません。問題の本体は RSC 側だからです。
/_next/static/* は Worker より前に Workers Assets が返すので、immutable のままです。404 の private, no-cache, no-store, ... も変わりません。つまりファイル名にハッシュが付いたものは長く持ち、それを指す側は毎回確かめる、という形に戻しただけです。
確かめたこと
opennextjs-cloudflare build → preview で、修正の前後のヘッダーを curl で比べました(GET のみ)。
| リクエスト | 前 | 後 |
|---|---|---|
| HTML | s-maxage=31536000, stale-while-revalidate=2592000 |
public, max-age=0, must-revalidate |
RSC(RSC: 1) |
同上 | public, max-age=0, must-revalidate |
/_next/static/chunks/* |
immutable |
変わらず |
| 404 | private, no-cache, no-store, ... |
変わらず |
テストは「修正を外すと落ちる」ことも確認しています。elparaiso では全 8 ページの HTML と RSC の計 16 件が同じ値になりました。既にブラウザに残っている古いエントリは、次に使われたときの裏の再検証で置き換わるので、そこまでは旧挙動が 1 回残ることがあります。
8 本への展開
10/20 に resume #80、ai-primer #90、utility-tracker #71、elparaiso #60、ai-news-feed-app #62 が入り、lifeplan-simulator #112 と service-anatomy #146 もキャッシュ割り込みの導入と一緒に同じ書き換えを持ちました。housing-performance-simulator だけ漏れていて、10/26 のセキュリティレビューで見つかり #94 で直しました。横展開は「一度に全部やったつもり」が一番漏れます。今はこの 8 本すべてに入っています。