
字节笔记本
2026年10月7日 · 约 8 分钟读完
会话中间件里,next 为什么要先放行
这段中间件本身很短:每个匹配到的请求都进 updateSession,把 Supabase 的会话从 Cookie 里读出来,需要时再写回去。真正要看懂的是匹配规则、NextResponse.next 的参数,以及为什么示例里先创建了一个看起来马上又会被换掉的响应。

中间件只包一层会话
项目里的 middleware.ts 不自己解析用户,它把请求交给 updateSession:
export async function middleware(request: NextRequest) {
return await updateSession(request)
}config.matcher 用否定预查跳过不该碰会话的路径:_next/static、_next/image、favicon.ico,以及 svg、png、jpg、jpeg、gif、webp。静态资源没有用户会话,每次都去刷新 Cookie 只会拖慢图片。其余路径,包括页面和接口,都会进这个函数。
有人会把“中间件”分成路由中间件和接口中间件两种产品。在这个文件里它们不是两套运行时。匹配到的请求走同一条函数,页面和 Route Handler 都算。想让某个接口不刷新会话,改的是 matcher,不是再写一个框架。
NextRequest 上日常会碰到的是 URL、方法、头和 Cookie。会话逻辑用的是 Cookie,不是从查询字符串里取令牌。
next 是放行,参数是改写这次放行
NextResponse.next() 表示继续原来的路由,不要在中间件里把响应终结掉。它可以带一个对象。对话里整理过三块:
request:改继续往下传的请求,比如换一组头headers:加在这次响应上的头rewrite:内部改目标地址,会盖掉原来的 URL
会话刷新用的是第一种的一种变体:先 NextResponse.next({ request }),让下游仍看到这次请求,同时拿到一个可以挂 Set-Cookie 的响应对象。不调用 next,也不返回重定向或 JSON,这条请求就没有合法的中间件返回值。
先创建的那个响应,后来会被 setAll 换掉
updateSession 用 @supabase/ssr 的 createServerClient。Cookie 适配器有两头。getAll 从 request.cookies 读出现有会话。setAll 在令牌需要刷新时被调用:先把新 Cookie 写进请求,再新建一个 NextResponse.next,把同样的 Cookie 写到响应上,这样浏览器下次才会带上新值。
于是出现一个很准的疑问:函数开头那个 NextResponse.next() 的结果,后面并没有被直接返回,setAll 里又创建了一次。开头那次是不是多余?
在“这次请求刚好刷新了令牌”的路径上,开头那个对象确实被丢掉了。但令牌还新鲜、setAll 不会被调用时,函数必须仍然返回一个放行响应。开头那次创建就是为了这条路径。如果删掉它,又只在 setAll 里赋值,变量在不刷新的请求上是空的,中间件要么不能编译,要么返回空。
优化如果要做,应该是:默认放行对象先放好,setAll 里用新对象替换它,函数末尾永远 return 那个变量。不要只删掉初始化。官方示例写成先 next 再在 setAll 里重建,看起来重复,是为了让读和写两条路径都有响应。
环境变量用的是公开的 Supabase 地址和匿名键。服务角色密钥不要放进中间件,这段代码跑在每次请求的边上,密钥进了包就会跟着到边缘节点。
放行对象要在两条路径上都存在
匹配规则决定谁进入会话刷新。NextResponse.next 是放行,不是“执行下一行业务代码”。令牌要刷新时,setAll 会换一个新的放行响应并把 Cookie 写上去。令牌不用刷新时,开头那个放行响应负责返回。删掉其中一条,会话要么不更新,要么请求直接没有响应。

matcher 写错时,症状像登录坏了
否定预查把静态文件排除在外。如果正则把某个页面路径也排除了,那个页面不会刷新会话,用户会觉得只有那一个地址掉登录。如果正则什么都不排除,图片请求也会进 updateSession,边缘函数的调用量会跟着静态资源走。改 matcher 之后,用一张图片和一个需要登录的页面各请求一次,确认图片没有 Set-Cookie 的额外开销,页面有。
请求对象上能改的不只有 Cookie。next 的 request.headers 可以往下游塞一个标记,例如请求已经过中间件。不要把访问令牌复制进自定义头再传到浏览器可见的响应里。响应头和请求头在那个参数对象里是两套。会话 Cookie 走 Set-Cookie,不走一个自造的 x-token 响应头。
有人会问 next 是不是“就执行下一行”。在中间件文件里,next 不是调用你写在下面的那个函数。文件里通常只有 middleware 一个导出函数。next 把控制权交回 Next 的路由系统,由它继续找页面或 Route Handler。你在 middleware 下面再写的辅助函数,不会因为 next 就被框架调用。
setAll 里除了写响应,还要写回 request.cookies。同一次请求后面的服务器组件读的是请求上的 Cookie。只写响应、不写请求,浏览器下次会带新令牌,但这一次渲染仍读到旧的。两边都写,才是同一次点击里登录态连续。
匿名键可以出现在这段代码里,因为它本来就会下发到浏览器。把服务角色密钥放进同样的环境变量名字里,是部署错误,不是中间件能判断的。代码审查时看 createServerClient 的第二个参数是不是匿名键那一个变量。
边缘运行时里不要在中间件做数据库查询。会话刷新已经是每次导航的成本。再加一次业务查询,会把中间件变成第二个后端。业务数据放在页面或 Route Handler。中间件只负责让 Cookie 里的会话在过期前被换新。 匹配器是字符串数组,可以写多条。一条复杂的否定预查出错时,很难用眼睛看出来。可以先写成几条明确要保护的路径前缀做试验,确认会话刷新发生在页面上,再收成一条正则。试验完不要把过宽的匹配留在生产。过宽的意思是静态资源也进了函数。
日志不要打印完整 Cookie。会话值就是登录凭据。中间件如果为了调试把请求头全打出来,等于把凭据写进日志系统。需要确认刷新是否发生时,打一个布尔值:这次有没有调用 setAll。够用了。
本地开发时,中间件改完有时要重启,而不是只靠热更新。行为没变,先确认跑的是保存后的文件,再怀疑 next 的参数。请求一条需要登录的页面,看响应里有没有会话 Cookie 被续上。没有,再回到 getAll 是否读到了旧值。 边缘环境里这个文件能用的 API 也更少。不要在 updateSession 里读文件系统或做长连接。它的工作是一次 Cookie 读写。超出这个范围的依赖,会让中间件在本地能跑、部署后启动失败。失败的表现是所有页面 500,而不是某一个接口。
把 matcher 和 updateSession 分开测。matcher 用日志看函数是否进入。updateSession 用一次过期会话看 Cookie 是否被换新。两件事叠在一起时,会不知道是没进函数,还是进了但没写响应。



