服务器错误TypeError:无法读取未定义对象的属性(读取 'replace') Next.js
我遇到了这个错误,但我真的不知道从哪里开始调试。有人能帮我吗?
我的系统起初工作正常。我想把一个用户包裹到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架构:
jwtDecrypt(需要一个五部分的加密JWE)jwtVerify(需要一个三部分的签名JWS)
当你修改代码来添加 "user package" 时,你很可能只是对令牌进行了签名(SignJWT),但不小心跳过了外层的加密步骤(EncryptJWT)。
如果你把一个三部分的JWS传入 jwtDecrypt,jose 库会按 . 将令牌分割,并尝试对第四和第五部分进行base64解码。因为这两部分现在是 undefined、jose 的内部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/headers 的 cookies(),这可能会导致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