\Amazon ポイントDEAL祭り開催中(22日まで!)/

Next.jsの「Failed to find Server Action “ServerActionID”.」を固有のビルドIDを設定して解消する

記事内に広告が含まれていることがあります。

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”.

このエラーは以下の環境にて再現できます。

  1. 定期的にServerActionsが呼び出されるコードを定義
  2. Next.jsプロジェクトをビルド npm run build
  3. サーバースタート npm start
  4. 該当ページをブラウザで開く→そのまま開きっぱなしにしておく
  5. 再度Next.jsをビルド npm run build
  6. 再度サーバースタート npm start
  7. 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に実装されています。

actionIdactionModIdが見つからなかった場合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_ENVdevelopmentであればそのまま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回確認していますが、サーバーの負担にならない程度の設定を行うと良いです。

今回Reactコンポーネントとして実装していますが、タイマーの管理等はReactのライフサイクル内で行った方が楽です。

もちろん通常のTypeScript関数を作っても良いのですが、React内で使うのであればReactコンポーネントとして実装した方が実装も再利用も手軽だと思います。

使い方

後はRefreshコンポーネントを任意の場所で呼び出せばOKです。

もしプロジェクト全体に適用したい場合、appディレクトリ直下のlayout.tsxbodyタグ内においてあげると良いでしょう。

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. 1度アクセスがあってユーザーがページを開きっぱなしにしていた
  2. このページ内で定期的にServerActionsが呼び出されていた
  3. その状態で新しくビルドが行われデプロイされた

というケースに限ります。

そのため、理想的なユースケースとしてはやはり該当コンポーネント内でのみ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を損なわないような設計が大事です。

スポンサーリンク
プログラミング
スポンサーリンク
\この記事いいね!と思ったらシェアしてね/
スポンサーリンク

コメント

タイトルとURLをコピーしました