Next.js Middleware追加user_id到请求体报错排查及替代方案(Supabase)
问题背景
我正在开发一个基于Supabase的Next.js项目,Supabase的users表存储已认证用户,每个用户分配uuid作为user_id。用户填写表单注册为员工时,数据需要存入employees表(该表主键为users表的user_id),但表单提交的API请求体未携带user_id。尝试通过Middleware从用户会话获取user_id并追加到请求体时,出现TypeScript错误:扩展后的对象缺少cookies、geo等NextRequest核心属性。
当前middleware.ts代码
import { NextRequest, NextResponse, Body } from 'next/server'; import { createSupabaseReqResClient } from '@/lib/supabase/server-clients'; export async function middleware(request: NextRequest) { let modifiedRequest = request; const supabase = createSupabaseReqResClient(request, NextResponse.next()); const { data: { session } } = await supabase.auth.getSession(); const user = session?.user; if (!user && request.nextUrl.pathname.startsWith('/my-profile')) { return NextResponse.redirect(new URL('/', request.url)); } if (request.nextUrl.pathname === '/api/employee') { const body = await request.json(); const user_id = user?.id; const modifiedBody = new Body(JSON.stringify({ ...body, id: user_id })); modifiedRequest = { ...request, body: modifiedBody, }; } return NextResponse.next(modifiedRequest); } export const config = { matcher: ['/my-profile','/api/employee'], };
错误信息
Type '{ body: any; [INTERNALS]: { cookies: RequestCookies; geo: { city?: string | undefined; country?: string | undefined; region?: string | undefined; latitude?: string | undefined; longitude?: string | undefined; } | undefined; ip?: string | undefined; url: string; nextUrl: NextURL; }; ... 18 more ...; text(): Promise<....' is missing the following properties from type 'NextRequest': cookies, geo, ip, nextUrl, and 3 more.ts(2740) let modifiedRequest: NextRequest
解决方案
一、修复Middleware的错误
NextRequest是类实例,无法通过对象展开(...request)完整复制其私有属性和方法,直接修改会导致TypeScript类型不匹配。正确做法是创建新的NextRequest实例,保留原请求的核心属性,再通过NextResponse.rewrite转发修改后的请求。
修正后的middleware.ts代码:
import { NextRequest, NextResponse } from 'next/server'; import { createSupabaseReqResClient } from '@/lib/supabase/server-clients'; export async function middleware(request: NextRequest) { const supabase = createSupabaseReqResClient(request, NextResponse.next()); const { data: { session } } = await supabase.auth.getSession(); const user = session?.user; // 未登录访问/my-profile时重定向到首页 if (!user && request.nextUrl.pathname.startsWith('/my-profile')) { return NextResponse.redirect(new URL('/', request.url)); } // 处理/api/employee请求,仅当用户已登录时修改请求体 if (request.nextUrl.pathname === '/api/employee' && user) { // 读取原请求体 const body = await request.json(); // 追加user_id到请求体(注意字段名需与employees表主键一致,这里改为user_id) const modifiedBody = JSON.stringify({ ...body, user_id: user.id }); // 构造新的NextRequest实例,保留原请求的核心配置 const modifiedRequest = new NextRequest(request.url, { method: request.method, headers: request.headers, body: modifiedBody, cookies: request.cookies, }); // 转发修改后的请求到原API路由 return NextResponse.rewrite(request.nextUrl, modifiedRequest); } return NextResponse.next(); } export const config = { matcher: ['/my-profile','/api/employee'], };
二、其他传递user_id到API的方案
除了Middleware,还有几种更简洁的实现方式:
1. 客户端直接追加user_id到请求体
在客户端组件中通过Supabase的钩子获取当前用户ID,提交表单时直接加入请求体:
// 客户端表单组件 import { useUser } from '@supabase/auth-helpers-react'; export default function EmployeeForm() { const user = useUser(); const handleSubmit = async (formData: FormData) => { if (!user) return; // 未登录时不提交 const formPayload = Object.fromEntries(formData); // 追加user_id到请求数据 const payload = { ...formPayload, user_id: user.id }; const res = await fetch('/api/employee', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); // 处理响应逻辑 if (res.ok) { alert('注册成功'); } }; return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.target as HTMLFormElement)); }}> {/* 表单字段 */} <button type="submit" disabled={!user}>提交</button> </form> ); }
优点:实现简单,无需Middleware;缺点:依赖客户端状态,需确保用户已登录。
2. 服务端API路由直接获取user_id
在API路由内部通过Supabase获取当前用户会话,直接使用用户ID插入数据,无需修改请求体:
// app/api/employee/route.ts import { createSupabaseReqResClient } from '@/lib/supabase/server-clients'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const supabase = createSupabaseReqResClient(request, NextResponse.next()); const { data: { session } } = await supabase.auth.getSession(); const user = session?.user; if (!user) { return NextResponse.json({ error: '未授权访问' }, { status: 401 }); } const body = await request.json(); // 插入数据时直接使用用户ID作为employees表的主键 const { error } = await supabase .from('employees') .insert({ ...body, user_id: user.id }); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ success: true }); }
优点:安全性高,user_id由服务端获取,客户端无法篡改;缺点:每个需要用户ID的API路由需重复获取会话逻辑(可封装为公用函数)。
3. 通过请求头传递user_id
在Middleware中将user_id加入请求头,API路由从请求头读取:
// middleware.ts中的处理逻辑 if (request.nextUrl.pathname === '/api/employee' && user) { const headers = new Headers(request.headers); headers.set('X-User-ID', user.id); return NextResponse.next({ headers }); }
API路由中读取:
// app/api/employee/route.ts export async function POST(request: NextRequest) { const userId = request.headers.get('X-User-ID'); const supabase = createSupabaseReqResClient(request, NextResponse.next()); const { data: { session } } = await supabase.auth.getSession(); // 验证用户会话,防止请求头被伪造 if (!userId || session?.user.id !== userId) { return NextResponse.json({ error: '未授权访问' }, { status: 401 }); } const body = await request.json(); const { error } = await supabase.from('employees').insert({ ...body, user_id: userId }); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ success: true }); }
优点:无需修改请求体,适合固定格式的请求;缺点:需额外验证请求头的合法性,防止伪造。
内容的提问来源于stack exchange,提问作者Noor Fatima

