🗂

Next.jsのmiddlewareはVercel以外でも問題なく使えるか

2021/10/27に公開3

Next.jsでv12〜middlewareという機能が使えるようになりました。

https://nextjs.org/docs/middleware

middlewareに書いた処理はリクエストが完了する前に実行されます。Cookieの値に応じてルーティングを振り分けたり、Basic認証を導入したり等など、幅広い用途で使えそうです。

VercelとNext.jsの組み合わせが強いのは、VercelにNext.jsをデプロイするとこのmiddleware部分をEdge Functionsで捌いてくれるという点です。つまり、静的なページに対するリクエストに対して、オリジンサーバーに触れことなくmiddlewareを実行できるということです。

Vercel以外のプラットフォームだとどうなのか

ドキュメントには以下のような記載があります。

This works out of the box using next start, as well as on Edge platforms like Vercel, which use Edge Functions.

Vercel以外のプラットフォームでnext build + next startを使ってNext.jsを動かす場合もmiddlewareは使えそうです。

ただ、ひとつ気になるのは特定のページをCDNにキャッシュしたいようなケースです。たとえばCloud RunやApp Engineで動かすような場合、middleware部分もオリジンサーバーで実行するしかありません。CDNから(オリジンサーバーに触れず)キャッシュを配信するようなページではmiddlewareをどう動かすのでしょうか。

このあたりを軽く試してみたのでまとめておきます。

Next.jsをnext startで動したときのmiddlewareを使ったときの挙動を検証

先に結論を書いておくと、CDNを配置していない構成であればmiddlewareは全く問題なく使えます。また、CDNを置いている場合も、ページのCache-Controlヘッダの値を書き換えるような処理を書いていない限り、問題なく使えそうです。

※ あくまでもNext.js v12.0.0時点での検証結果です。バージョンが変わると結果が異なる可能性があります。

使用したmiddleware

検証のためにmiddlewareでレスポンスヘッダにx-middleware-modified-at: タイムスタンプという値を付与します。

pages/_middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  const res = NextResponse.next();
  res.headers.set("x-middleware-modified-at", new Date().getTime().toString());
  return res;
}

これをApp Engineにデプロイしてみます。ページをリロードしたときにx-middleware-modified-atの値が更新されていればmiddlewareが問題なく動いていると言えます。

get○○Propsなしのページ → 👌

サーバーサイドでのpropsの取得の処理(get○○Props)が書かれていないページでは、Next.jsのビルド(next build)時にHTMLファイルが生成されます。

ページに直接アクセスがあったときにはオリジンサーバーからこのHTMLファイルが返されます。デフォルトではCache-Controlprivateとなっており、CDNを置いている場合も(HTMLファイルを強制的にキャッシュする設定になっていない限り)CDNにキャッシュされません。オリジンサーバーに到達するため、middlewareは問題なく動きます。

getStaticPropsを使ったページ → 👌

※ ISR(revalidateオプションを有効にするケース)はそもそもnext startで動かすことが難しいため、ここでは検証の対象外とします。

getStaticPropsで書かれたページも、next startで動かしている場合はオリジンサーバーからHTMLが返却されるため、middlewareは問題なく動きます。

普通にgetServerSidePropsgetInitialPropsを使ったページ → 👌

こちらもオリジンサーバーからレスポンスが返されるため問題なしです。

getServerSidePropsでCache-Controlを上書きした場合 → middlewareに処理された後のレスポンスがキャッシュされる

唯一問題となりうるのは、getServerSidePropsgetInitialPropsの中でCache-Controlをいじっているようなケースです。

pages/foo.tsx
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  
  res.setHeader("Cache-Control", "public, max-age=300");
  
  return {
    props: { ... },
  };
};

例えば、上のサンプルではCache-Controlの値をpublic, max-age=300にすることで、レスポンスを300秒間CDNやブラウザにキャッシュされるようにしています。

Cache-Controlが置き換わるのはデプロイ先のGCPによるものでした

Cache-Controlが置き換わると書いていましたが、これはNext.jsによるものではなく、デプロイ先のApp Engineのエッジキャッシュの仕様によるもののようです。レスポンスヘッダにSet-Cookieヘッダが存在して場合はCDNにキャッシュされないようにCache-Controlを書き換えてくれるっぽいです。

https://cloud.google.com/cdn/docs/caching#non-cacheable_content

👆 こちらはCloud CDNのドキュメントですが、App Engineのエッジキャッシュもこの部分は同じ仕様なのだと思われます。@divertaさん情報ありがとうございました。

以下、修正前の内容です。


middlewareでCookieを付与する処理を書くと、レスポンスヘッダのCache-Controlの値がprivate, max-age=300に書き換えられることが分かりました(publicprivateになる)。

つまり、middlewareは問題なく動くものの、ページはCDNにキャッシュはされなくなるということです。

色々と試してみると、middlewareでリダイレクトの処理をかけただけのケースではCache-Controlの値が書き換えられることはありませんでした。

Next.jsのソースコードを探ってみましたが、この書き換えを行っている部分を見つけられず、どのようなロジックなのかが把握できていません。また何か分かったら追記します(誰か知っている方がいたら教えてください)。

この場合、middlewareに処理された後のレスポンスがCDNにキャッシュされます。つまり、リロードしてもキャッシュが有効な間はレスポンスヘッダのx-middleware-modified-atは同じ値のままになります。

そのため、リクエスト元のリージョンやcookie、User-Agentの値に応じてレスポンスを切り替えるといったことは難しそうです。

まとめ

  • next startでNext.jsを動かしている場合もmiddlewareは問題なく動かせそう
  • CDNキャッシュが有効なページではmiddlewareが処理した後のレスポンスがキャッシュされる。リクエストヘッダの値に応じてレスポンスを切り替えたい場合にはCDNキャッシュは諦める必要がある

自分のNext.jsのプロジェクトでは後者の影響を受ける可能性があるので、とりあえずキャッシュ優先でmiddlewareの使用はやめておこうと思っています。

Vercel以外のプラットフォームでCDNキャッシュとmiddleware的な処理を両立させる必要がある場合、いさぎよくCloudflare WorkersやCloudFront FunctionsなどのEdgeで動くFaaSを導入するのが良いかもしれません。

Discussion

かとけんかとけん

Google CDNを使ってる場合(GAEやCloud Run)は、set-cookieがレスポンスヘッダーにあると、privateに書き換えられる場合があるので、そっちの要因じゃないでしょうか?

catnosecatnose

仰る通りCache-Controlの置き換えはGAEのエッジキャッシュによるものでした(この部分はCloud CDNと同じ仕様のようです)。記事を修正しました。
ご指摘ありがとうございました。