You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 14邮件验证流程构建时动态服务器使用错误排查

Next.js 14:邮件验证流程构建时出现动态服务器使用错误

我正在使用Next.js 14开发邮件验证流程,开发环境下一切正常,但构建时出现了与API路由相关的动态服务器使用错误。

架构概述

  • 用户点击邮件中的验证链接:验证链接指向类似/verify?token=abcd1234的前端页面。
  • 前端页面(/verify):从URL中获取token,向后端API路由发送请求以验证token。
  • 后端API路由(/api/auth/verify):处理token,与数据库进行校验,返回包含验证状态的JSON响应。

相关前端代码

'use client';

import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { Nav } from "@/app/components/Nav";
import React, { Suspense } from 'react';

export default function Verify() {
  const [message, setMessage] = useState('Verifying...');
  const searchParams = useSearchParams();
  const token = searchParams.get('token');

  useEffect(() => {
    async function verifyToken() {
      if (token) {
        try {
          const res = await fetch(`/api/auth/verify?token=${token}`);
          const data = await res.json();

          console.log(data);
          if (data.status === 'success') {
            setMessage('Congratulations! Your email has been successfully verified.');
          } else if (data.status === 'expired') {
            setMessage('Verification token has expired. Please request a new one.');
          } else {
            setMessage('Verification failed. Please try again or contact support.');
          }
        } catch (error) {
          setMessage('An error occurred during verification. Please try again or contact support.');
        }
      }
    }

    verifyToken();
  }, [token]);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Nav />
      <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-yellow-700">
        <div className="bg-white p-8 rounded shadow-md text-center dark:bg-yellow-500">
          <h1 className="text-2xl font-bold mb-4 dark:text-white">Email Verification</h1>
          <p>{message}</p>
        </div>
      </div>
    </Suspense>
  );
}

相关后端API路由代码

import { db } from '@vercel/postgres';

export async function GET(request) {
  try {
    const { searchParams } = new URL(request.url);
    const token = searchParams.get('token');

    if (!token) {
      return new Response(
        JSON.stringify({ status: 'error', message: 'No token provided' }),
        {
          status: 400,
          headers: {
            'Content-Type': 'application/json',
          },
        }
      );
    }

    const client = await db.connect();

    const tokenQuery = `
      SELECT user_id, created_at, used FROM schema."email_verification" WHERE token = $1
    `;
    const tokenResult = await client.query(tokenQuery, [token]);

    if (tokenResult.rows.length === 0 || tokenResult.rows[0].used) {
      client.release();
      return new Response(
        JSON.stringify({ status: 'error', message: 'Invalid or used token' }),
        {
          status: 400,
          headers: {
            'Content-Type': 'application/json',
          },
        }
      );
    }

    const { user_id, created_at } = tokenResult.rows[0];
    const tokenAge = (new Date() - new Date(created_at)) / 1000 / 60; // in minutes
    const tokenExpiry = 4320; // 3 days in minutes

    if (tokenAge > tokenExpiry) {
      client.release();
      return new Response(
        JSON.stringify({ status: 'expired', message: 'Token expired' }),
        {
          status: 400,
          headers: {
            'Content-Type': 'application/json',
          },
        }
      );
    }

    const updateQuery = `
      UPDATE schema."user"
      SET confirmed = true
      WHERE user_id = $1
    `;
    const result = await client.query(updateQuery, [user_id]);

    if (result.rowCount === 0) {
      client.release();
      return new Response(
        JSON.stringify({ status: 'error', message: 'Failed to update user' }),
        {
          status: 500,
          headers: {
            'Content-Type': 'application/json',
          },
        }
      );
    }

    // Mark the token as used
    const markTokenUsedQuery = `
      UPDATE schema."email_verification"
      SET used = true
      WHERE token = $1
    `;
    await client.query(markTokenUsedQuery, [token]);

    client.release();

    return new Response(
      JSON.stringify({ status: 'success', message: 'Email verified successfully' }),
      {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
        },
      }
    );
  } catch (error) {
    console.error("Error verifying email:", error);
    return new Response(
      JSON.stringify({ status: 'error', message: 'Internal server error' }),
      {
        status: 500,
        headers: {
          'Content-Type': 'application/json',
        },
      }
    );
  }
}

构建错误信息

Dynamic server usage: Route /api/auth/verify couldn't be rendered statically because it accessed `request.url`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error

已尝试的解决方法

  • 确保useSearchParams被包裹在Suspense边界内
  • 调整前后端逻辑,确保JSON响应和正确的错误处理
  • 在前端使用useEffect确保验证请求只发送一次

解决方案

1. 修复API路由的动态依赖问题

错误根源是API路由/api/auth/verify在构建时被尝试静态渲染,但它使用了request.url,这属于动态依赖。Next.js 14中,API路由默认是静态渲染的,除非明确标记为动态。

你可以选择两种方式解决:

方式一:强制路由动态渲染

在API路由顶部添加指令,强制该路由始终在服务器端动态执行:

import { db } from '@vercel/postgres';

// 添加这行,强制路由动态渲染
export const dynamic = 'force-dynamic';

export async function GET(request) {
  // 原有代码保持不变
}

方式二:使用Next.js规范的URL参数获取方式

用request.nextUrl替代new URL(request.url),这是Next.js提供的更适配的API:

// 替换原有获取token的代码
const token = request.nextUrl.searchParams.get('token');

2. 优化前端页面的Suspense使用

前端页面的Suspense包裹位置不需要包含整个组件,useSearchParams在客户端组件中已被正确处理,可移除不必要的Suspense包裹:

'use client';

import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { Nav } from "@/app/components/Nav";

export default function Verify() {
  const [message, setMessage] = useState('Verifying...');
  const searchParams = useSearchParams();
  const token = searchParams.get('token');

  useEffect(() => {
    async function verifyToken() {
      if (token) {
        try {
          const res = await fetch(`/api/auth/verify?token=${token}`);
          const data = await res.json();

          console.log(data);
          if (data.status === 'success') {
            setMessage('Congratulations! Your email has been successfully verified.');
          } else if (data.status === 'expired') {
            setMessage('Verification token has expired. Please request a new one.');
          } else {
            setMessage('Verification failed. Please try again or contact support.');
          }
        } catch (error) {
          setMessage('An error occurred during verification. Please try again or contact support.');
        }
      }
    }

    verifyToken();
  }, [token]);

  return (
    <>
      <Nav />
      <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-yellow-700">
        <div className="bg-white p-8 rounded shadow-md text-center dark:bg-yellow-500">
          <h1 className="text-2xl font-bold mb-4 dark:text-white">Email Verification</h1>
          <p>{message}</p>
        </div>
      </div>
    </>
  );
}

3. 额外优化建议

  • 前端请求改用POST方法,将token放在请求体中,避免URL暴露敏感信息,同时减少URL参数相关潜在问题
  • 在API路由中使用数据库事务,确保user表更新和email_verification标记为已用的操作原子性,避免数据不一致

内容的提问来源于stack exchange,提问作者Miki U

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 18:42:35