とろろこんぶろぐ

かけだしR&Dフロントエンジニアの小言

App Router移行時に0.01%の確率でCSR遷移が404エラーになる

概要

Pages Router から App Router 移行時に一部既存の画面での CSR 遷移が 404 エラーになりました。

この件について調査したので、記事にしてまとめておきます。

前提

今回発生したバグの内容の再現環境の特徴として、以下が挙げられます。

Base Path について

Base Path の設定は next.config.js に以下のような記載があると、

module.exports = {
  basePath: '/base',
}

/pages/examples.tsx で配置したページコンポーネントが、

URL /base/examples で閲覧できるようになるものです。

また以下のような Link コンポーネントは、自動的に Next.js によって

<Link href="/examples">Example Page</Link>

/base/examples に画面遷移するようなリンクに置き換えてもらえます。

システムの制約上、我々のアプリでは Base Path の設定が必要だったためこの設定を入れています。

実際に起きたこと

特定の画面Aへの画面遷移で 404 のエラーが発生するようになりました。

原因調査

調査したところ以下のことがわかりました。

  • 404 エラーになるのは、 Base Path が二重に付与されているため発生する (ex. /base/base/examples になってしまう) 。
  • URL 直遷移では問題が発生せず、CSR によるクライアントルーティングでのみ発生する。
  • ローカルでの実行時 next dev では発生しない (next build & next start で発生する) 。
  • 画面A へのリンクのパスは間違っていない。
  • App Router との共存をやめると発生しなくなる。

原因の深掘り

原因背景

我々のアプリは基本的に Pages Router を使って開発されています。

このエラーが発生する1つ前のリリースで、将来的な App Router 移行を見据えて、試験的に1画面での App Router リプレイスを実施しました。

つまり、このエラー発生時から App Router と Pages Router の共存状態 になっていました。

ちなみに該当の App Router 化した画面単体で見ると、機能・UI の変更は一切なく E2E テストを用意した上での移行だったこともあり、デグレを発生させずにリリースすることができていました。

しかし、この Pages Router と App Router との共存が起きてしまったことで、全く関係のない画面での画面遷移で 404 エラーが発生 することになりました。

Next.js のクライアントルーティング内部ロジック

Next.js では App Router と Pages Router が共存して存在する場合、 Pages Router からの画面遷移で該当のパスが App Router かどうかハンドリングする箇所があります。

ここには Bloom Filter が使われています。

この Bloom Filter は確度高く判別するわけではなく、ある程度高速に該当のリンクパスが App Router か Pages Router のものか判別するために使われています。 この機能は 0.01% の確率で False Positive する ことが明記されています。

Next.js のドキュメント

Routing: Linking and Navigating | Next.js

実際のコード

next.js/packages/next/src/shared/lib/bloom-filter.ts at 4f142dc58b0c70fe2cdadb3e2e1cc7463a14b4cd · vercel/next.js · GitHub

なぜ 0.01% の確率で False Positive してもいいかというと、結果的に実行される処理は同じだからだと思われます。

クライアント側で CSR でのルーティング時に Bloom Filter によるフィルタ処理が走って、

  • マッチした場合、エラー表出なしで適切にソフトナビゲーション・ハードナビゲーションが行われる
  • マッチしなかった場合、エラー表出した上でハードナビゲーションが行われる

という動きになります。

つまり、結果的に該当のパスにはハードナビゲーションされるので、最低限のユーザーへの機能提供は担保されているという想定だと思われます。

Next.js のクライアントルーティング

Base Path 設定 x App Router 共存時の問題

しかし、ここで Base Path 設定がついている場合には問題が発生します。

すでに Base Path が付与された状態でフィルタ判定されるにも関わらず、ハードナビゲーションの処理に対して、再度 Base Path を付与するような処理が入っています。

next.js/packages/next/src/shared/lib/router/router.ts at 4efe14238b5ab11935e73aa09631ef5ec8045b13 · vercel/next.js · GitHub

つまりこれによって、 Base Path が二重に付与されてしまうことでハードナビゲーションし直されたとしても該当の画面が存在しない 404 エラーになる、という現象が起きてしまっていました。

発生したエラーの要因

解決策(ワークアラウンド

こちらはすでに下記の Issue により報告されています。

github.com

しかしながら、上記 Base Path の付与ロジック部分の修正ではなく、このフィルタ機能自体をオフにすることが紹介されています。

// next.config.js
experimental: {
    clientRouterFilter: false,
},

本質的な解決策ではないものの、結果的にこのワークアラウンドによって問題は解消されてはいます。

Next.js v13.5.6 でのエラー率

実はこの問題は上記で 0.01% と書いていたものの、我々のアプリの Next.js のバージョンである、 v13.5.6 ではエラー率は 1% でした。

現在の v14 系以降ではエラー率が見直され、 0.01% にまで下がっています。

github.com

これによって、我々のアプリではより顕著に問題が発生しやすくなっていたという問題もありました。

雑記

ちなみに、自分の理解が間違っていなければ Base Path が設定されている場合、 App Router 側に配置しているパスに対しても適切にハンドリングができていないと思われます。

getRouteInfo で Base Path を取り除いたパスを取得するはずですが、

next.js/packages/next/src/shared/lib/router/router.ts at 4efe14238b5ab11935e73aa09631ef5ec8045b13 · vercel/next.js · GitHub

next.js/packages/next/src/shared/lib/router/router.ts at 4efe14238b5ab11935e73aa09631ef5ec8045b13 · vercel/next.js · GitHub

getRouteInfo 内で Pages Router 側の build manifest からは App Router 配下の manifest は取得できないので、エラーがスローされます。

next.js/packages/next/src/client/route-loader.ts at 2db296e4fa97570b7e487f8778024543366b82b2 · vercel/next.js · GitHub

これによって、結果としてエラーが出力されてハードナビゲーションされる、という動きになっていると思われます。

おわりに

Pages Router から App Router 移行時に一部既存の画面での CSR 遷移が 404 エラーになってしまったバグを調査してまとめたものを記事に残しました。

Base Path の設定がある状態で、Pages Router と App Router の共存状態を作る場合には注意した方が良さそうです。

現在は 0.01% の確率で False Positive するだけなので危険性は低いものの、 clientRouterFilter オプションはオフにしておいた方が無難かなと思いました。

Next.js の Bloom Filter での False Positive が実際に発生することを確認したレポジトリ

github.com