服务器错误TypeError:无法读取未定义对象的属性(读取 'replace') Next.js

前端开发 2026-07-10

我遇到了这个错误,但我真的不知道从哪里开始调试。有人能帮我吗?

我的系统起初工作正常。我想把一个用户包裹到cookie令牌中,使用户变成Premium,但失败了。

能否指点我如何调试并检查错误出在哪儿?我尝试移除中间件,结果一切又恢复正常。

错误截图

在此输入图片描述

my middleware.ts

const handleI18nRouting = createMiddleware(routing);

function detectLocale(req: NextRequest) {
  const country = req.headers.get('x-vercel-ip-country');

  if (country === 'VN') return 'vi';
  if (country === 'CN') return 'zh';

  // fallback when running locally
  const language = req.headers.get('accept-language');

  if (language?.startsWith('vi')) return 'vi';
  if (language?.startsWith('zh')) return 'zh';

  return 'en';
}

export async function middleware(req: NextRequest) {
  const path = req.nextUrl.pathname;

  // bỏ qua api và static
  if (
    path.startsWith('/api') ||
    path.startsWith('/_next') ||
    path.includes('.')
  ) {
    return NextResponse.next();
  }

  // kiểm tra đã có locale chưa
  const hasLocale = /^\/(vi|zh)(\/|$)/.test(path);
  if (!hasLocale) {
    const locale = detectLocale(req);

    // english = root
    if (locale === 'en') {
      return handleI18nRouting(req);
    }

    const url = req.nextUrl.clone();
    url.pathname = `/${locale}${path}`;

    return NextResponse.redirect(url);
  }
  console.log('4');

  // verify session
  const session = await verifySession();

  const isAdminRoute = Object.values(adminUrl).some(
    p => path === p || path.startsWith(`${p}/`),
  );

  const isProtectedRoute = Object.values(protectedUrl).some(
    p => path === p || path.startsWith(`${p}/`),
  );

  const authPages = [
    '/authentication/sign-in',
    '/authentication/sign-up',
    '/authentication/forgot-password',
  ];

  const isAuthPage = authPages.includes(path);

  if (!session && (isProtectedRoute || isAdminRoute)) {
    return NextResponse.redirect(new URL('/authentication/sign-in', req.url));
  }

  if (session && isAdminRoute && !session.isAdmin) {
    return NextResponse.rewrite(new URL('/unauthorized', req.url));
  }

  if (session && isAuthPage) {
    return NextResponse.redirect(new URL('/', req.url));
  }

  return handleI18nRouting(req);
}

export const config = {
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};

verifySession.ts

import { cookies } from 'next/headers';
import { cache } from 'react';
import { decrypt } from './session';
type SessionPayload = {
  userId: string;
  isAdmin: boolean;
  subscriptionTier?: string;
  subscriptionExpiredAt?: string | Date;
};

export const verifySession = cache(async () => {
  try {
    const cookie = (await cookies()).get('session')?.value;

    if (!cookie || typeof cookie !== 'string') return null;

    const session = (await decrypt(cookie)) as SessionPayload;
    if (!session?.userId) {
      return null;
    }

    const expiredAt = session.subscriptionExpiredAt
      ? new Date(session.subscriptionExpiredAt)
      : null;

    const isVip = expiredAt ? expiredAt > new Date() : false;

    return {
      isAuth: true,
      userId: session.userId,
      isAdmin: session.isAdmin,
      isVip,
      subscriptionTier: session.subscriptionTier ?? 'FREE',
      subscriptionExpiredAt: expiredAt,
    };
  } catch (error) {
    console.error('verifySession error:', error);
    return null;
  }
});

decrypt.ts

export async function decrypt(token: string) {
  if (!token || typeof token !== 'string') {
    throw new Error('Invalid token input');
  }

  try {
    const { payload } = await jwtDecrypt(token, ENC_KEY);

    if (!payload?.jws || typeof payload.jws !== 'string') {
      throw new Error('Invalid token structure');
    }

    const { payload: verifiedPayload } = await jwtVerify(payload.jws, SIGN_KEY);

    return verifiedPayload;
  } catch (err) {
    console.error('DECRYPT ERROR:', err);
    throw err;
  }
}

解决方案

这段错误可能是因为某个库在尝试解码你JWT令牌中缺失或格式不正确的一段内容。

在你的 decrypt.ts 文件中,你使用了嵌套的JWT架构:

  1. jwtDecrypt(需要一个五部分的加密JWE)
  2. jwtVerify(需要一个三部分的签名JWS)

当你修改代码来添加 "user package" 时,你很可能只是对令牌进行了签名(SignJWT),但不小心跳过了外层的加密步骤(EncryptJWT)。

如果你把一个三部分的JWS传入 jwtDecryptjose 库会按 . 将令牌分割,并尝试对第四和第五部分进行base64解码。因为这两部分现在是 undefinedjose 的内部base64解码器在调用 undefined.replace(/-/g, '+') 时崩溃,导致你遇到的正是这个错误。

确保你的令牌生成逻辑仍然把已签名的令牌包裹在一个 new EncryptJWT(...) 中。或者,你可以在 decrypt.ts 增加一个安全检查,以防检测到格式错误的令牌时把整个服务器都崩溃:

export async function decrypt(token: string) {
  if (!token || typeof token !== 'string') {
    throw new Error('Invalid token input')
  }

  // Safety check: A valid JWE must have 5 parts.
  // This prevents the 'replace' TypeError inside the jose library.
  if (token.split('.').length !== 5) {
    console.error('DECRYPT ERROR: Token is not a valid 5-part JWE')
    return null
  }

  try {
    const { payload } = await jwtDecrypt(token, ENC_KEY)
    if (!payload?.jws || typeof payload.jws !== 'string') {
      throw new Error('Invalid token structure')
    }
    const { payload: verifiedPayload } = await jwtVerify(payload.jws, SIGN_KEY)
    return verifiedPayload
  } catch (err) {
    console.error('DECRYPT ERROR:', err)
    return null
  }
}

另外我注意到,你的 verifySession.ts 文件在调用从 next/headers 导入的 cookies()

在Next.js中,next/headers 主要用于服务端组件和路由处理程序。中间件在Edge运行时执行,不能完全支持来自 next/headerscookies(),这可能会导致Next.js内部头部解析器崩溃。

你可以将 verifySession.ts 更新为接受一个可选的cookie参数,并从 middleware.ts 明确传递cookie字符串。

import { cookies } from 'next/headers';
import { cache } from 'react';
import { decrypt } from './session';

// ... SessionPayload type ...

// Accept an optional cookie parameter so Middleware can pass it explicitly
export const verifySession = cache(async (middlewareCookie?: string) => {
  try {
    // Read from parameter (if called by middleware), fallback to next/headers (if called by Server Components)
    const cookie = middlewareCookie ?? (await cookies()).get('session')?.value;

    if (!cookie || typeof cookie !== 'string') return null;

    const session = (await decrypt(cookie)) as SessionPayload;
    if (!session?.userId) {
      return null;
    }

    const expiredAt = session.subscriptionExpiredAt ? new Date(session.subscriptionExpiredAt) : null;
    const isVip = expiredAt ? expiredAt > new Date() : false;

    return {
      isAuth: true,
      userId: session.userId,
      isAdmin: session.isAdmin,
      isVip,
      subscriptionTier: session.subscriptionTier ?? 'FREE',
      subscriptionExpiredAt: expiredAt,
    };
  } catch (error) {
    console.error('verifySession error:', error);
    return null;
  }
});
// ... previous middleware code
    console.log('4');

    // Read the cookie from req.cookies and pass it to verifySession
    const sessionCookie = req.cookies.get('session')?.value;
    const session = await verifySession(sessionCookie);

    const isAdminRoute = Object.values(adminUrl).some(
    // ... rest of your code
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章