2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js + Supabase magic link が1ヶ月壊れていた — NextResponse.redirect() の Cookie 落とし穴と smoke test 設計

2
Last updated at Posted at 2026-05-07

結論:NextResponse.redirect() は Cookie を引き継がない

Next.js App Router の Route Handler で、await cookies() でセッション Cookie を書いた後に NextResponse.redirect() を返すと、その Cookie が redirect レスポンスに乗りません。

// ❌ 壊れている実装(Cookie が redirect に届かない)
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
await cookies().set('session', ...)
return NextResponse.redirect(next)

// ✅ 正しい実装(redirect レスポンスに直接 Cookie をセット)
const response = NextResponse.redirect(next)
const supabase = createServerClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  {
    cookies: {
      getAll: () => request.cookies.getAll(),
      setAll: (cookies) =>
        cookies.forEach(({ name, value, options }) =>
          response.cookies.set(name, value, options)
        ),
    },
  }
)
await supabase.auth.exchangeCodeForSession(code)
return response

この仕様に気づくまで、筆者の masatoman.net では launch 以来約 1 ヶ月、誰もログインできない状態が続いていました。


なぜ気づけなかったのか

302 で「成功に見える失敗」

auth callback は HTTP 302 を返していたので、アクセスログには何も出ません。

  • Vercel デプロイ → success
  • magic link メール → 届く
  • callback URL → HTTP 302(成功に見える)
  • セッション → 未確立(ここが壊れていた)

型チェックも linter も ESLint も検知できない種類のバグです。

smoke test がなかった

デプロイが成功した ≠ 機能している。この前提がなく、実際にブラウザでログインを試みなければ気づけませんでした。


auth callback の正しい実装

// app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createServerClient } from '@supabase/ssr'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/'

  if (!code) {
    return NextResponse.redirect(`${origin}/auth/error`)
  }

  const response = NextResponse.redirect(`${origin}${next}`)

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) => {
            request.cookies.set(name, value)
            response.cookies.set(name, value, options)
          })
        },
      },
    }
  )

  const { error } = await supabase.auth.exchangeCodeForSession(code)

  if (error) {
    return NextResponse.redirect(`${origin}/auth/error`)
  }

  return response
}

response.cookies.set() が正解。cookies() からセットするのではなく、redirect レスポンスオブジェクトに直接書き込みます。


合わせて踏んだ Stripe Webhook の 301 問題

同時期に Stripe Webhook も 1 週間全失敗していました。

原因は Stripe に登録した URL が naked ドメイン(masatoman.net)で、Vercel が www.masatoman.net に 301 リダイレクト。Stripe は POST → 301 を追わない(body 喪失のセキュリティ仕様)ため全件失敗。

# Stripe ダッシュボードのエラーレスポンス
{
  "redirect": "https://www.masatoman.net/api/stripe/webhook",
  "status": 301
}

修正:Stripe の Webhook URL を www. 付きに変更するだけ。


smoke test の実装

// app/api/health/auth-callback/route.ts
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { origin } = new URL(request.url)
  const res = await fetch(
    `${origin}/auth/callback?code=smoke_test_invalid`,
    { redirect: 'manual' }
  )
  const location = res.headers.get('location') ?? ''
  const ok =
    res.status === 302 &&
    (location.includes('/auth/error') || location.includes('/'))

  return NextResponse.json(
    { ok, status: res.status, location },
    { status: ok ? 200 : 500 }
  )
}
# .github/workflows/smoke-test.yml
name: Smoke Test
on:
  push:
    branches: [main]
  schedule:
    - cron: '0 */6 * * *'
jobs:
  auth-callback:
    runs-on: ubuntu-latest
    steps:
      - name: Auth Callback Health Check
        run: |
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
            https://your-site.com/api/health/auth-callback)
          if [ "$STATUS" != "200" ]; then exit 1; fi

main push のたびに自動実行。収益化インフラの常時監視として機能します。


まとめ:やること 3 つ

  1. auth callback の実装を確認cookies().set()NextResponse.redirect() の順なら今すぐ修正
  2. Stripe Webhook URL を確認 — Vercel のリダイレクト先(www/naked)と一致させる
  3. smoke test を main push に組み込む — デプロイ成功 ≠ 機能している

収益化インフラの品質設計については masatoman.net の全文記事で詳しく解説しています。

👉 Next.js + Supabase magic link が1ヶ月壊れていた(masatoman.net)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?