返回博客

Next.js App Router 下 _ 前缀目录路由静默消失,middleware 把 404 伪装成 403

某次给一个后台管理系统加了一批健康探活接口,路径统一放在 src/app/api/_health/src/app/api/_debug/ 下。本地 next dev 跑着没问题,上了测试环境后所有探活灯全红。第一反应是鉴权拦截了——curl 一看,返回的确实是 403 Forbidden。折腾了半小时 middleware 白名单,白名单加进去,403 变成了 404。这才意识到问题根本不是鉴权,而是这些路由从来就没注册过

现象

src/app/api/_health/route.ts    → GET /api/_health 返回 404
src/app/api/_debug/stats/route.ts → GET /api/_debug/stats 返回 404

文件存在,route.ts 正确导出了 GET handler,next build 也没报错——但访问就是 Not Found。

更坑的地方:项目里有一层全局 middleware 做 UA 或 IP 鉴权,它在所有 path 上都会触发,包括根本不存在的路径。于是访问一个未注册的路由,得到的不是 404,而是 middleware 先一步返回的 403,完美隐藏了真实症状。

根因

Next.js 13+ App Router 规范里,_ 开头的目录是 private folder,整个目录树都不会被注册为路由,这是框架的刻意设计——让你可以把组件、工具函数、schema 等文件和路由文件共存在 app/ 里,而不用担心它们被当成页面或 API。

src/app/
  _components/    ← private,不是路由
  _lib/           ← private,不是路由
  api/
    _health/      ← private,不是路由!
      route.ts
    users/        ← 正常路由 /api/users
      route.ts

Pages Router 没有这套机制,pages/api/_xxx.ts 在老路由下是正常的 API 路由。从 Pages Router 迁移到 App Router 时,如果沿用了 _ 前缀的命名习惯,这些路由会静默消失,没有任何编译警告。

坑:middleware 把 404 伪装成 403,让你误以为路由存在只是被拒

全局 middleware 对所有 pathname 都 fire,包括压根不存在的路径。当你访问一个未注册的 _xxx 路由时,middleware 先返回 401/403/redirect,你看到"鉴权错误",以为路由存在,开始改白名单——白名单加进去,middleware 放行,Next.js 路由器查不到 → 才暴露出真实的 404。诊断的 ground truth 应该是 npx next build 的 route table,而不是 curl 的响应码。

正解

第一步:用 build 产物确认

# 查看 build 输出的路由列表
npx next build 2>&1 | grep "_health\|_debug"
# 什么都没有 = 确认中招
 
# 或直接看产物目录
ls .next/server/app/api/
# _health 不在其中 = 中招

第二步:重命名,去掉下划线前缀

git mv src/app/api/_health  src/app/api/health
git mv src/app/api/_debug   src/app/api/debug
# 新建未 track 的目录用普通 mv,git mv 会报源目录为空

同步更新所有引用:

// 调用方 fetch
- fetch('/api/_health')
+ fetch('/api/health')
 
// middleware 白名单(若有)
- const WHITELIST = ['/api/_health', '/api/_debug']
+ const WHITELIST = ['/api/health', '/api/debug']

第三步:build 验证

npx next build 2>&1 | grep "health\|debug"
# 期望看到:
# ├ ƒ /api/health   0 B   0 B
# ├ ƒ /api/debug    0 B   0 B
正解:去掉 _ 前缀,用 build route table 而非 curl 确认路由是否注册

重命名后执行 npx next build,在输出的 route table 里看到目标路径才算真正修好。实测 curl 前先确认路由已注册,否则 middleware 依然会干扰判断。

合法用途:private folder 正是用来放非路由文件的

如果你想在 app/ 目录里放共享组件、工具函数、类型定义,_components/_lib/_types/ 等带下划线的目录是推荐做法——它们不会被误识别为路由,正是 App Router 设计这套机制的初衷。(folder) 圆括号是 route group(不影响 URL 但参与路由),@folder 是 Parallel Routes,三种语法各有用途,不要混用。

一句话外卖

Next.js App Router 里,_ 前缀目录永远不是路由;全局 middleware 会把这类 404 伪装成鉴权错误——诊断时先看 npx next build 的 route table,别信 curl 的状态码。