Next.js App Router middleware の cookie が Cloudflare Pages で動かない原因

Next.js App Router middleware の cookie が Cloudflare Pages で動かない原因

※本記事にはプロモーション(広告・アフィリエイトリンク)を含みます。

Cloudflare Pages に Next.js App Router をデプロイし、middleware.ts で cookie を書いているのに「セットされない」「読めない」という問題は、原因が複数同時に絡みやすい。この記事では動かない原因を3つのパターンに整理し、それぞれの対処をコードで示す。

Cloudflare Pages で Next.js が動く仕組みの前提

Next.js を Cloudflare Pages にデプロイするには、公式アダプター @cloudflare/next-on-pages(執筆時点)を使う。このアダプターが Next.js のビルド出力を Cloudflare Workers(Edge Runtime)向けに変換する。

通常の next start(自前 Node.js サーバー)との最大の違いはランタイムだ。

実行環境 ランタイム Node.js API
next start(VPS・自宅サーバー等) Node.js フル利用可
Cloudflare Pages(next-on-pages) Workers / Edge 制限あり
Next.js middleware(どの環境でも) Edge Runtime 固定 制限あり

重要なのは、Next.js の middleware.ts はどの環境にデプロイしても必ず Edge Runtime で動くという点だ。Cloudflare Pages 固有の制約もあるが、middleware の cookie 問題の多くは Cloudflare かどうかに関係なく再現する。

参考までに、私自身の EC サイトは Cloudflare Tunnel + systemd 常駐の Node.js サーバーで動いており、middleware は Node.js ランタイム上で動作している。この記事は Cloudflare Pages(Workers ランタイム)の挙動を公式ドキュメントと仕様から整理したものだ。

原因 1:next/headers の cookies() を middleware で使っている

最も多いパターンがこれだ。

// ❌ middleware.ts — これは動かない
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const cookieStore = cookies() // Edge Runtime では利用不可
  cookieStore.set('session', 'value')
  return NextResponse.next()
}

cookies() は Server Component や Route Handler 用の関数で、middleware では使えない。Next.js 公式ドキュメントにも「cookies() is not supported in Middleware」と明示されている。Cloudflare 固有の問題ではなく、ローカルの next dev でも同様に動かない。

middleware で cookie を扱うには request.cookies(読み込み)と response.cookies.set()(書き込み)を使う。

// ✅ middleware.ts — 正しい書き方
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  // 読み込み
  const token = request.cookies.get('token')?.value

  // 書き込み
  const response = NextResponse.next()
  response.cookies.set('token', 'new-value', {
    httpOnly: true,
    secure: true,        // 本番(HTTPS)では必須
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24, // 24時間(秒単位)
  })
  return response
}

原因 2:Cloudflare の CDN キャッシュが Set-Cookie を落としている

Cloudflare の CDN はリクエスト経路にキャッシュを挟む。キャッシュヒットしたレスポンスには Set-Cookie ヘッダーが含まれないため、ブラウザまで cookie が届かない。

確認方法はブラウザの DevTools「Network」タブでレスポンスヘッダーを見ること。cf-cache-status: HIT が付いているリクエストに Set-Cookie がない、というパターンが典型だ。

対処は2つある。

① レスポンスに Cache-Control: no-store を付ける

const response = NextResponse.next()
response.headers.set('Cache-Control', 'no-store, no-cache')
response.cookies.set('token', value, { httpOnly: true, secure: true, path: '/' })
return response

② Cloudflare の Cache Rules でパスをキャッシュ対象から除外する

Cloudflare ダッシュボードの「キャッシュ」→「Cache Rules」で、認証が絡む URL パス(/api/* や /login など)に「Bypass Cache」を設定する。旧来の Page Rules でも設定できるが、現在は Cache Rules が推奨されている(執筆時点)。

原因 3:matcher の設定漏れ

middleware が対象のパスで動いているかを確認する。

// middleware.ts
export const config = {
  matcher: ['/dashboard/:path*'],
}

上記では /login へのリクエストで middleware が走らない。/login で cookie をセットしたいなら matcher に含める必要がある。

matcher を書かない場合はすべてのパスに適用されるが、_next/static などの静的ファイルにも適用されてパフォーマンスに影響する。定番のパターンは以下だ。

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

middleware が実際に動いているかは、console.log を仕込んで wrangler pages deployment tail コマンドのログで確認できる。

middleware でセットした cookie を Server Component から同一リクエスト内で読む

response.cookies.set() でセットした cookie は、同じリクエストを処理している Server Component からはすぐに読めない。これは Cloudflare Pages に限らない Next.js の基本動作で、新しい cookie 値は次のリクエストから有効になる。

同一リクエスト内で Server Component に値を渡したい場合は、cookie ではなくカスタムリクエストヘッダーを使う。

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-user-id', userId)

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  })
  // クライアントへの cookie セットは response 側に書く
  response.cookies.set('token', value, { httpOnly: true, secure: true, path: '/' })
  return response
}
// app/dashboard/page.tsx
import { headers } from 'next/headers'

export default async function DashboardPage() {
  const headersList = await headers()
  const userId = headersList.get('x-user-id')
  // ...
}

Web 標準の Request オブジェクトはイミュータブルなため、ヘッダーを変更するには必ず new Headers(request.headers) でコピーを作る必要がある。Workers ランタイム上での Request/Response の扱い方については Hono × Cloudflare Workers で SSE を実装した記事 も参考になる。

まとめ

症状 確認・対処
cookie がそもそもセットされない middleware で next/headers の cookies() を使っていないか確認
DevTools に Set-Cookie ヘッダーが来ない cf-cache-status を確認し、Cache-Control: no-store を追加
特定のページだけ middleware が動かない config.matcher にそのパスが含まれているか確認
Server Component から新しい値が読めない NextResponse.next({ request: { headers } }) でリクエストヘッダー経由に変更
どれでもない @cloudflare/next-on-pages と Next.js のバージョン互換を公式ドキュメントで確認

デバッグの入口は DevTools「Network」タブのレスポンスヘッダー確認だ。Set-Cookie がヘッダーに含まれていないなら Cloudflare のキャッシュか middleware のコードを疑い、Set-Cookie は届いているのに cookie が読めないなら httpOnly / SameSite / Secure の属性設定を見直す。まずここから切り分けてほしい。

この記事で触れたもの

よくある質問

Cloudflare Pages で Next.js の middleware.ts は動きますか?

動きます。`@cloudflare/next-on-pages` アダプター経由で Cloudflare Workers(Edge Runtime)上で実行されます。ただし Node.js 固有の API は使えず、`next/headers` の `cookies()` など一部の関数は利用不可です。バージョン互換も公式ドキュメントで確認が必要です。

middleware でセットした cookie を同じリクエストの Server Component から読めますか?

`response.cookies.set()` でセットした cookie は次のリクエストから有効になるため、同一リクエスト内の Server Component からはすぐに読めません。値を渡す場合は `NextResponse.next({ request: { headers } })` でカスタムリクエストヘッダーを付与する方法が有効です。

`next/headers` の `cookies()` は middleware で使えますか?

使えません。Server Component や Route Handler 向けの関数です。middleware では `request.cookies`(読み込み)と `response.cookies.set()`(書き込み)を使います。これは Cloudflare Pages 固有の制約ではなく Next.js の仕様で、ローカルの `next dev` でも同様に動きません。