Next.jsはAppRouterでServerActionsが導入されましたが、ServerActions関数は毎ビルド時固有のIDを生成します。
ブラウザで実行中のアプリで持っているIDとサーバー側のIDが異なる場合、Next.jsのコンソール上には「Failed to find Server Action “ServerActionID”. This request might be from an older or newer deployment. Original error: Cannot read properties of undefined (reading ‘workers’)」というエラーが出ることになります。
そこで「ユニークな値(=タイムスタンプ)をビルドプロセス時環境変数として設定し、60秒に1回確認を行い新しいビルドがあったら強制的にブラウザをリロードさせる」という仕組みを導入します。
Failed to find Server Action “ServerActionID”.
このエラーは以下の環境にて再現できます。
- 定期的にServerActionsが呼び出されるコードを定義
- Next.jsプロジェクトをビルド
npm run build
- サーバースタート
npm start
- 該当ページをブラウザで開く→そのまま開きっぱなしにしておく
- 再度Next.jsをビルド
npm run build
- 再度サーバースタート
npm start
- ServerActionsが実行される度にコンソールにエラーが表示される
GitHubのIssue(#58431、#697565)を見ると他にも様々なケースで同様のエラーが発生するようですが、基本的には「ServerActionsのIDが見つからないためServerActionsのヘルパーであるWorkersが見つからないよ」というエラーのようです。
AppRouterのServerActionIDとWorkers
AppRouterで導入されたServerActionsはビルド時に関数ごとにユニークなIDが付与されるようです(この記事ではこのIDをServerActionIDと呼称しています)。
ServerActionsではWorkersという機能が活躍しており、WorkersはServerActionsの呼び出し、実行、依存関係の解決等を担っています。
WorkersはServerActionsを適切に実行するために必要で、ServerActionsIDと紐づけられているので知らないServerActionIDを使って呼び出そうとしても「おめーは誰だ!」となる訳ですね・・・!(厳密にNext.jsのコードを覗いたわけではないので若干憶測が含まれますが大まかなエラー内容はこんな感じだと思います)
Failed to find Server Action~というエラーは/src/server/app-render/action-handler.tsに実装されています。
actionId
とactionModId
が見つからなかった場合throwされてエラーメッセージが出力されています。
function getActionModIdOrError(
actionId: string | null,
serverModuleMap: ServerModuleMap
): string {
try {
// if we're missing the action ID header, we can't do any further processing
if (!actionId) {
throw new Error("Invariant: Missing 'next-action' header.")
}
const actionModId = serverModuleMap?.[actionId]?.id
if (!actionModId) {
throw new Error(
"Invariant: Couldn't find action module ID from module map."
)
}
return actionModId
} catch (err) {
throw new Error(
`Failed to find Server Action "${actionId}". This request might be from an older or newer deployment. ${
err instanceof Error ? `Original error: ${err.message}` : ''
}`
)
}
}
ServerActionIDはビルド時に変わる(時もある)
ServerActionIDはビルド時にファイルのpath、アクション(関数)名によって生成されるようです。
ServerActionsIDは.next/server/server/reference-manifest.json
で確認できます。
{
"node": {
"005a5295945e9c552cd09a8158820de14d93939115": {
"workers": {
"app/page": {
"moduleId": "841",
"async": false
}
},
"layer": {
"app/page": "action-browser"
}
}
},
"edge": {},
"encryptionKey": "FXwOke4qNM7Eh7FMPGhuzQXcL4ciPma/wHrkUJzKyDM="
}
例えばこのjsonだと005a5295945e9c552cd09a8158820de14d93939115
がServerActionsIDになります。
ちなみにこのServerActionID、ビルドによって必ず変わるケースもあれば全く同一のケースもあります。
詳しく処理を追っていないのですが、何かしらの内部実装をID生成に利用しているのかもしかしたらモジュールの呼び出し順等が影響しているかもしれません。
今回のエラーはID生成時に前回と異なるIDが生成される場合に限ります。
Next.jsビルド時にユニークIDを設定する
ここまでの整理で、Next.jsを再ビルドしてServerActionIDが変わってしまい、デプロイした時点でサーバー上のServerActionIDとすでに開いているブラウザ側が持っているServerActionIDの相違でエラーになることはわかりました。
その上で解決策としてビルド時にユニークIDを設定し環境変数に設定を行い、フロントから定期的にServerActionIDの検証を行い相違があった場合強制的にブラウザをリロードするという方法を実装します。
環境変数 NEXT_PUBLIC_APP_VERSION
NEXT_PUBLIC_APP_VERSIONという環境変数をビルド時に定義し、ビルド時点でのユニークIDを設定します。
環境変数名はなんでも良いですが、NEXT_PUBLIC_
というプレフィックスを付けないとフロント側から参照できませんので必ずNEXT_PUBLIC_xxx
という形式を採用してください。
ここでは例としてタイムスタンプを生成していますが、ユニークであれば何でも良いです。
例えばGitHub Actions等のCIでデプロイする場合、デプロイブランチへのコミットID等を使って動的にNEXT_PUBLIC_APP_VERSIONを生成する等が考えられます。
const nextConfig: NextConfig = {
// envセクションを追記
env: {
NEXT_PUBLIC_APP_VERSION: process.env.NODE_ENV === 'development' ? 'development' : Date.now().toString(),
},
};
module.exports = nextConfig;
process.env.NODE_ENV
がdevelopment
であればそのままdevelopment
という値を、development
(=開発環境)ではないならDate.now()
関数を使ってタイムスタンプを文字列で設定しています。
getAppVersion.ts
'use server';
export const getAppVersion = async () => {
return process.env.NEXT_PUBLIC_APP_VERSION || 'development';
};
getAppVersion
はサーバー側のNEXT_PUBLIC_APP_VERSIONを取得します。
もし何らかの理由で取得できなかった場合はdevelopment
という文字列を返します。
Reflesh.tsx
'use client';
import { useEffect } from 'react';
import { getAppVersion } from '../_actions/getAppVersion';
import { useRouter } from 'next/navigation';
export const Refresh = () => {
const router = useRouter();
useEffect(() => {
const interval = setInterval(async () => {
const serverVersion = await getAppVersion();
const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION;
if (serverVersion !== currentVersion) {
router.refresh();
}
}, 60000);
return () => clearInterval(interval);
}, [router]);
return null;
};
Refleshコンポーネントはフロント側が持っているNEXT_PUBLIC_APP_VERSION
とサーバー側のNEXT_PUBLIC_APP_VERSION
を比較しています。
もし両者が違ったらroute.refresh()
でブラウザを強制リロードします。
ここでは60000ms(=60秒)に1回確認していますが、サーバーの負担にならない程度の設定を行うと良いです。
使い方
後はRefresh
コンポーネントを任意の場所で呼び出せばOKです。
もしプロジェクト全体に適用したい場合、appディレクトリ直下のlayout.tsx
のbody
タグ内においてあげると良いでしょう。
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="ja" className="font-bold text-slate-700">
<body className={`${defaultFont.className} flex min-h-screen flex-col antialiased`}>
<Refresh /> // bodyタグ内に設置
<h1 className="py-4 text-center text-lg md:py-12 md:text-2xl">
<Link href="/">{title}</Link>
</h1>
{children}
</body>
</html>
);
}
注意点
この実装はいくつか考慮するべき注意点があります。
サーバーへの負担
やっている事は非常にシンプルですが、上記のとおりに実装をした場合1分に1回クライアント側からアクセスがあります。
個人制作レベルのWebアプリならともかく、大規模でアクセスが多いWebアプリの場合過剰なサーバー負担になる可能性もあります。
- ServerActionsIDのチェック間隔を長くする
layout.tsx
ではなく、必要な(=ServerActionsを利用している)コンポーネント内でのみ実行する
このような対策が必要です。
そもそも今回のケースの場合、
- 1度アクセスがあってユーザーがページを開きっぱなしにしていた
- このページ内で定期的にServerActionsが呼び出されていた
- その状態で新しくビルドが行われデプロイされた
というケースに限ります。
そのため、理想的なユースケースとしてはやはり該当コンポーネント内でのみRefreshコンポーネントを呼び出す方が良いでしょう。
フォームのクリア
もしRefreshコンポーネントを採用したページに入力フォームがあった場合、何も対策をしないと入力フォームはクリアされることになると思います。
例えば思いつくのはIDのバリデーションでしょうか、リアルタイムで「このIDは既に使われています」と出るアレです。
パっと考えられる事として、一度入力された内容は一時的にlocalStorageに保存させてリロードされた時に存在確認、もしデータがあったフォームに反映する等の対策が考えられます。
が、ユーザーサイドとしてはやはり入力中に画面が更新されるのはびっくりするので極力入力フォームがあるページには採用しない方が良いかなあと思います。
Next.jsの「Failed to find Server Action “ServerActionID”.」をビルドIDを設定して解消する まとめ
コンソール上に「Failed to find Server Action “ServerActionID”. This request might be from an older or newer deployment. Original error: Cannot read properties of undefined (reading ‘workers’)」というエラーが発生した場合、ServerActionIDが見つけられなかった事が原因となります。
冒頭で記述したようにそれ以外にもエラー発生原因はありそうですが、もし今回のケースのように定期実行するServerActionsがあった場合は高確率でこの対策が有効になるはずです。
ただしフォームがあるページに採用する場合には設置を慎重に検討すべきでしょう、UXを損なわないような設計が大事です。
コメント