
字节笔记本
2026年10月6日 · 约 12 分钟读完
Next.js 请求处理三剑客详解
Next.js 项目里,绝大多数和请求路径有关的需求,比如加安全响应头、旧链接跳转、接口代理,最终都会落到 next.config.js 的三个配置上:headers、redirects 和 rewrites。三者都作用在请求进入路由之前,但分工不同:headers 负责给响应附加自定义标头,redirects 负责把请求转向新地址,rewrites 则在用户无感知的情况下把请求映射到另一个目标。理解它们的差异,是理清 Next.js 请求处理的第一步。

一、headers:自定义 HTTP 响应头
1.1 基本用法
headers 用于给匹配到的请求设置自定义响应标头,配置是一个异步函数,返回规则数组:
module.exports = {
async headers() {
return [
{
source: '/about',
headers: [
{ key: 'x-custom-header', value: 'my custom header value' },
{ key: 'x-another-header', value: 'another value' },
],
},
]
},
}source 是匹配路径,headers 数组的每一项就是一条要写入响应的标头。访问 /about 的每个响应都会带上这两个自定义头。
1.2 三种路径匹配写法
headers、redirects、rewrites 共用同一套路径匹配规则,掌握一处即可三处通用。
第一种是基础参数匹配,冒号开头的片段会被捕获,可以在 value 中直接引用:
{
source: '/blog/:slug',
headers: [{ key: 'x-slug', value: ':slug' }],
}第二种是通配符匹配,* 表示匹配任意层级的后续路径:
{
source: '/blog/:slug*',
headers: [{ key: 'x-slug', value: ':slug*' }],
}第三种是正则匹配,在参数名后面用括号书写正则,比如只匹配纯数字的文章 ID:
{
source: '/blog/:post(\\d{1,})',
headers: [{ key: 'x-post', value: ':post' }],
}1.3 常用安全响应头
headers 最典型的用途是集中配置安全相关的标头:
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
],
},
]
},
}这几项各有分工:X-DNS-Prefetch-Control 控制浏览器是否对页面中的域名做 DNS 预读取;Strict-Transport-Security 强制浏览器在 max-age 指定的两年内走 HTTPS,同时覆盖子域名并支持进入预加载列表;X-Frame-Options 限制页面只能被同源页面嵌入 iframe,防止点击劫持;X-Content-Type-Options 让浏览器放弃内容嗅探,严格按声明的类型处理响应。
二、redirects:URL 重定向
2.1 基本用法
redirects 把匹配到的请求转向另一个地址:
module.exports = {
async redirects() {
return [
{ source: '/old-blog', destination: '/blog', permanent: true },
{ source: '/blog/:slug', destination: '/news/:slug', permanent: false },
]
},
}permanent 为 true 时返回 308 状态码,为 false 时返回 307。destination 里可以直接引用 source 捕获的参数,比如 :slug 会原样带到新地址,适合批量迁移栏目结构。
2.2 为什么用 307 和 308
传统 301、302 有一个历史包袱:浏览器跟随重定向时,可能把 POST 等非 GET 请求改写成 GET,请求体随之丢失。307 和 308 正是为解决这个问题而引入的:307 对应临时重定向,308 对应永久重定向,两者都保证重定向后的请求方法不变。凡是涉及表单提交或接口调用的跳转,都应该优先选择 307/308。
2.3 条件重定向:has 与 missing
redirects 还能根据请求自带的条件决定是否生效,has 表示满足条件才跳转,missing 表示缺少条件才跳转。下面的例子用 cookie 区分登录态:带 authorized=true 的用户访问 /dashboard 进入工作台,没带这个 cookie 的则被送到登录页:
module.exports = {
async redirects() {
return [
{
source: '/dashboard',
has: [{ type: 'cookie', key: 'authorized', value: 'true' }],
permanent: false,
destination: '/dashboard/home',
},
{
source: '/dashboard',
missing: [{ type: 'cookie', key: 'authorized' }],
permanent: false,
destination: '/login',
},
]
},
}条件类型除了 cookie,还支持 header、query 与 host,基本覆盖了常见的网关式分流需求。
三、rewrites:URL 重写
3.1 与 redirects 的核心区别
rewrites 同样把请求映射到另一个目标,但浏览器地址栏不会变化。redirects 是浏览器再发起一次请求,地址随之改变;rewrites 是服务端内部改写,用户看到的还是原地址,得到的却是另一份内容。这个特性让它天然适合两类场景:API 代理与渐进式迁移。
3.2 基本用法
module.exports = {
async rewrites() {
return [
{ source: '/about', destination: '/' },
{ source: '/blog/:slug', destination: '/news/:slug' },
]
},
}3.3 分阶段重写
需要更精细的控制时,rewrites 可以返回一个对象,把规则组织在三个阶段里:
module.exports = {
async rewrites() {
return {
beforeFiles: [
{ source: '/some-page', destination: '/somewhere-else' },
],
afterFiles: [
{ source: '/non-existent', destination: '/somewhere-else' },
],
fallback: [
{ source: '/:path*', destination: 'https://old-website.com/:path*' },
],
}
},
}三个阶段的执行位置不同:beforeFiles 在文件系统检查之前执行,命中立即改写;afterFiles 在静态文件与已有页面检查完之后、动态路由之前执行,适合只改写不存在的路径;fallback 排在所有路由尝试之后,典型用法是把整站请求兜底转发到旧站点,实现渐进式迁移。

3.4 代理外部 API
rewrites 最常见的用法之一是代理第三方接口:前端统一请求同域的 /api 前缀,由 Next.js 转发到真实服务,跨域问题就不存在了:
module.exports = {
async rewrites() {
return [
{ source: '/api/:path*', destination: 'https://api.example.com/:path*' },
]
},
}四、三者关系与使用建议
把三个配置放在一起看,可以归纳出四条结论:
第一,执行顺序是 headers、redirects、rewrites。响应头先追加,重定向再改变地址,重写最后兜底,同一条请求可能依次经过多层处理,规则的先后会直接影响结果。
第二,三者共用同一套路径匹配规则,参数捕获、通配符与正则全部通用,has/missing 条件判断也同样可用,学会一套即可全覆盖。
第三,涉及非 GET 请求的跳转优先使用 307/308,避免请求方法被浏览器改写,这是新旧状态码之间最实际的差别。
第四,迁移旧站时,先用 fallback 把流量兜底到旧站保证可用,再逐个路径用 redirects 或 rewrites 收编,是最稳妥的节奏。
理解了这三件工具的分工与顺序,next.config.js 里与请求处理相关的配置就基本没有盲区了。



