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 对所有 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重命名后执行 npx next build,在输出的 route table 里看到目标路径才算真正修好。实测 curl 前先确认路由已注册,否则 middleware 依然会干扰判断。
如果你想在 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 的状态码。