CDN、Next.js、Middlewareを使用している場合バージョン13に上げると動かなくなる場合がある

追記 2023/10/25

ミドルウェアを使用している際にX-Middleware-Prefetchヘッダを付与して送信すると{}がキャッシュされることで攻撃者は容易にキャッシュを{}にすることができるためこちらの問題にCVEが付きました。

13.4.20-canary.13で修正されているのでアップデートすることで解決するかと思います。

github.com

3行まとめ

  • Next.js 13からMiddlewareを使用しているとgetStaticProps以外を使っていてもprefetchを行うようになった。
  • Add middleware prefetching configgetServerSidePropsのprefetchを行った際のレスポンスボディが{}となった。
  • CDNでキャッシュしている場合、{}がキャッシュされてしまうことでページ遷移したときにデータが空になることがある。

前提

Next.js 13 あたりでAdd middleware prefetching configというPRがマージされました。

github.com

このPRの意味を理解する前に、現状のNext.jsのprefetchの仕様を理解する必要があります。現在のNext.jsのprefetchではgetStaticPropsで生成された静的ファイルは事前にprefetchを行います*1getStaticPropsではビルド時、ISRの際にはいい感じ*2に静的ファイルを作成をするので予めprefetchしてしまうことで高速にページに遷移することが可能となります。

しかし、Middlrewareを使用してパスを書き換えるなどの操作をしている場合、hrefに記載されているパスの先はgetStaticPropsを使用しているのかはたまたgetServerSidePropsを使用しているのかわかりません。そのため、この場合はページのSSR/SSG限らずに必ずprefetchを行う仕様*3*4となっているようです。

ですが、この仕様はgetServerSideProps*5, getInitialPropsで悪影響を及ぼします。prefetchにより意図しないタイミングで実行されたり、クライアントとサーバー両方で実行されてしまうのです。そのため、Middleware affecting getInitialProps after 12.2.0というissueが立ちました。
ちなみに、この問題はNextAuthでも問題になっているようでした。

この修正が、上で述べたPRという訳です。実際にはEnsure prefetch heuristic matches with and without middlewareというPRも前段にあります。変更内容は以下の通りです。

  1. experimentalmiddlewarePrefetchという設定を追加
  2. middlewarePrefetchがデフォルトのflexibleでは変わらずmiddlewareを使用しているとSSR/SSG限らずprefetchを行うがSSRの場合はgetServerSidePropsを評価せずに{}をレスポンスで返す*6
  3. middlewarePrefetchが、structの場合はmiddlewareを使用していてもhrefのパスが存在している場合(要はmiddlewareでパスを変更したりリダイレクトしていない場合)、SSGのみprefetchする。

デフォルトでは、middlewarePrefetchの設定はflexible*7であるためSSRでもprefetchが飛ぶようになっています。

何が問題か

この「SSRでもprefetchが飛び、レスポンスが{}で返る」というのが非常に問題となります。

prefetchのパスはhttps://example.com/_next/data/Vkcv8XUXemawzA1A9FCT3/ssr.jsonといった実際にそのページに遷移したときにサーバーから取得するパスと同じで、リクエストヘッダーにX-Middleware-Prefetch: 1という独自ヘッダーを付与してリクエストします。
Next.jsのサーバーはSSRの場合、X-Middleware-Prefetchヘッダーがついている場合は{}を返すといった挙動を取ります。つまり、X-Middleware-Prefetchのヘッダーの あり/なし でレスポンスデータが大きく変わってしまいます。

CDNを通している場合、/_next/*もキャッシュしてSSRでもISRみたいな挙動にするといったテクニックが使われていることが多いと思います。その場合、Next.jsのバージョンを13に上げたときにX-Middleware-PrefetchがCDNのキャッシュキーとなっていないと{}がキャッシュされるおそれがあります。
これにより、普通にアクセスすると問題ないが<Link>で遷移すると情報が空っぽになるといった問題*8が発生してしまいます(1敗)。

対策

一番簡単なのはCDNにX-Middleware-Prefetchをキャッシュキーとして追加することかと思います。

また、アプリケーションレベルでは、middlewarePrefetchの設定をstructにすることでも解決すると思われます。

実際にSSRでもprefetchを行っている例

github.com

*1:https://taroodr.com/posts/nextjs-prefetch

*2:説明が難しいので各自調べてほしい

*3:https://github.com/vercel/next.js/pull/39920

*4:いつから導入されたのか見つけられませんでしたがどうやら12.2.6-canary.1にはすでに導入されている?

*5:https://github.com/vercel/next.js/issues/39732

*6:SSRもSSGもしていないページのprefetchも{}が返るので合わせたという感じっぽいです。

*7:https://github.com/vercel/next.js/pull/42936/files#diff-1ce54af4c1a0166a265bebae9ba304b24816ebe9cbb92f90b9707379b0680e97R567

*8:https://github.com/vercel/next.js/pull/42936#discussion_r1027563953 PRのコメントでも言及されているがマージ後のコメントのためリアクションが0