🍺

Next.js App Router + NextAuth 環境の API Routes で Session が null になる場合の対処

2024/11/03に公開

前提

  • 以下 src ディレクトリ有効な前提です。src 無効にしている場合は都度読み替えてください
  • next-auth@beta はあくまで beta なので利用しておらず、 v4 における内容となります
  • 仕事の納期的な理由で懇切丁寧に実装全体を記載する余裕が現状ないので書き殴り気味なことをお許しください

課題: getServerSession が null になる

/api を使わずに直接 page.tsx で DB とのやり取りなどを実装する場合には発生しませんが、/api を BFF として使いたいケースで発生します

// src/app/(auth)/page.tsx
fetch('http://localhost:3000/api/hoge').then(res => res.json()).then(console.log)
// src/app/api/hoge/route.ts
const session = await getServerSession(authOptions)
console.log(session) // null

ログイン済みのブラウザで直接 /api/hoge にアクセスすると session 情報が console.log に反映されるが、 app/page.tsx 側から fetch すると null になる。

原因

NextAuth に認証情報の管理を任せきりで忘れそうになるが、単に Cookie で管理されている。
/src/app/**/*/page.tsx から /src/app/api/**/*/route.ts 側にこの Cookie が連動しないのが原因。
つまり headers から Cookie を取り出して fetch なりに渡してあげれば解決する。

対処方法

/// src/app/(auth)/page.tsx
import {headers} from 'next/headers'

fetch('http://localhost:3000/api/hoge', {
  headers: await headers(); // ここでは全部まとめて headers に詰め込んでいるが、 Cookie など必要なものに絞った方が better
})

hono + hc を使っているケース

API Routes をそのまま使うとバックエンドの実装から型推論してくれないので hono + hc でいい感じにしたくなりますよね。なったんです。
まず前提となる実装は以下のようなものとする。

// src/app/api/[[...routes]]/route.ts
// こうしておくとフロントエンドとバックエンドが共存するキモさが軽減される気がする。
export * from '@/server'
// src/server/index.ts
import { Hono } from "hono";
import { handle } from "hono/vercel";
import { hogeApi } from "./handlers/hoge";

export const runtime = "edge";

const app = new Hono().basePath("/api").route("/hoge", hogeApi);

export { app };

export const GET = handle(app);
export const POST = handle(app);
export const PUT = handle(app);
export const DELETE = handle(app);
// src/server/handlers/hoge.ts
import { Hono } from "hono";
import { getServerSession } from 'next-auth';
import { nextAuthOptions } from '@/auth';

const api = new Hono();

const routes = api.get("/", async (ctx) => {
  console.log(await getServerSession(nextAuthOptions));
  return ctx.json({ msg: "hoge" });
});

export { routes as hogeApi };
// src/server/types.ts
import { app } from ".";

export type AppType = typeof app;

以上のような形で api/* を hono で受け止めるようにしているとして、
普通に hc<AppType>('http://localhost:3000') するとまた Cookie が伝達されない。なぜなら Cookie がセットされてないから。
hc には init オプションが用意されているのでここで headers を指定すると解消できる。

そのため以下のようにした。

// src/server/client.ts
export function $hc() {
  return hc<AppType>('http://localhost:3000', {
    init: {
      headers: await header(),
    }
  })
}

これを使って page.tsx を実装する。以下のようなイメージ。

/// src/app/(auth)/page.tsx
export default function HogePage() {
  const cli = $hc();
  const res = cli.api.hoge();
  return <div>Hoge</div>
}

これで src/server/handlers 側で session が null にならなくなったはず。

Discussion