如果你在Nginx里改过URL,应该经历过这种时刻:rewrite规则写得清清楚楚,nginx -t也通过了,reload也没报错,可访问旧地址的时候,页面就是纹丝不动。打开浏览器控制台一看,状态码200,内容还是老页面。这时候你开始怀疑:是正则没匹配上?还是flag用错了?还是Nginx把配置缓存了?
实际上,大部分rewrite不生效或者产生异常跳转的案例,根因都不在"正则写没写对",而是对rewrite在Nginx请求处理流程里的执行时机、匹配对象、以及与location的交互方式理解不到位。这篇文章不打算照抄官方文档,而是把rewrite相关的几个真正影响实战的知识点拆开讲清楚,从执行阶段、语法细节、正则行为,到与location和proxy_pass的配合方式,最后附上我日常排查rewrite问题的完整方法和几个性能避坑建议。适合刚接触Nginx重定向配置的朋友,也适合那些已经被rewrite规则折磨过、想彻底搞懂原理的同行。
1. 先搞明白:Rewrite到底在Nginx的哪个环节起作用
1.1 一个让你沉默的排查场景
先说一个我见过很多次的案例。有人在server块里写了这样一条规则:
nginx复制server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass http://backend;
}
rewrite ^/api/(.*)$ /v2/$1 last;
}
他的本意是:只有访问 /api/ 前缀的请求才把路径改写成 /v2/,其他请求不受影响。但是实测下来,访问 http://example.com/ 首页时也出问题了,甚至所有请求都似乎被rewrite影响到了。
问题出在哪?server块级别的rewrite指令,不是在"匹配到某个location之后"才执行的,而是在进入location匹配之前就已经执行了。 很多人习惯性认为,配置写在哪个server里,就只对哪个server生效——这个理解没错;但"配置写在server块里,所以会在匹配location之后再处理"——这个理解是错的。
1.2 rewrite指令所处的位置决定了它的执行阶段
Nginx处理一个HTTP请求,会经历一个固定的内部流程。使用rewrite模块的指令时,实际相关的核心阶段可以简化为这四步:
- 接收请求,解析出URI
- 执行server块级别的rewrite指令(server rewrite阶段)
- 根据当前URI寻找匹配的location
- 执行location块级别的rewrite指令(location rewrite阶段)
这里有个关键差异:写在server块里的rewrite,会在寻找location之前执行。所以它一旦改写了URI,Nginx就会用改写后的URI去寻找location。这也是为什么很多人把rewrite写在server块里时,会发现"我明明只想改/api/,怎么好像所有请求都参与了匹配"——不是所有请求都被改写了,而是所有请求都会先经过这个rewrite的判断,虽然没有匹配的请求不会触发改写,但这个判断本身确实对每个请求都执行了。
与之对应,写在location块里的rewrite,则是在请求已经匹配到该location之后才执行。如果这个rewrite用了last标志,改写后的URI会重新走一次location匹配流程,可能会进入另一个location。
理解了这两个阶段,再回头看上面那个案例:rewrite ^/api/(.*)$ /v2/$1 last; 写在server块里,如果请求URI是 /api/user,它会先被改写成 /v2/user,然后再拿着 /v2/user 去匹配location。此时原来的 location /api/ 已经无法匹配了,如果你没有定义 location /v2/,请求就会落到默认location上,行为自然和预期完全不同。
1.3 规范化URI:rewrite匹配的到底是什么
rewrite的pattern匹配的不是浏览器地址栏里原始的URL,而是经过Nginx规范化(normalization)处理后的URI。这个规范化做了这几件事:
- 解码百分号编码,比如
%20会被解码成空格 - 合并连续的斜杠
/,比如/a//b会变成/a/b - 解析相对路径段,比如
/a/../b会变成/b - 去掉query string,也就是问号后面的参数部分
举个例子,请求 http://example.com/a%20b//c.html?name=test 进入rewrite阶段时,pattern实际匹配的是 /a b/c.html,而不是带query string的完整URI,更不是含编码的原始URI。
这个特性的实际影响是什么?如果你写了一组针对路径中空格、特殊字符的rewrite,要考虑编码解码后的效果;如果你在replacement里想要保留query string,则需要在replacement里显式处理。也有不少人在这里踩坑:正则里写 ^/search/(.*)$,实际请求是 /search/hello%20world?page=2,如果你期望capture到 hello world,那是可以的,因为URI已经被解码;但如果你在日志里看到的是编码后的内容,也别惊讶,因为Nginx内部不同阶段呈现的URI状态不一样。
这个阶段性的差异,是整个rewrite理解体系的基石。下面关于语法、flag选择、正则变量的所有内容,其实都是建立在这个执行流程之上的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 语法拆解:regex、replacement、flag的取舍逻辑
2.1 基本形态和最容易忽略的大小写问题
rewrite指令的基本形态只有一句话:
nginx复制rewrite regex replacement [flag];
regex是正则pattern,replacement是替换后的URI或完整URL,flag是可选标志。看起来简单,但第一个高频误区就藏在regex里:rewrite的pattern默认是大小写敏感的。
很多从location匹配转过来的人会在这里栽跟头。在location配置里,~* 是大小写不敏感的正则匹配,~ 是大小写敏感的;但rewrite指令本身没有这种修饰符,它单独出现时默认对大小写敏感。想要大小写不敏感,需要显式在正则开头加 (?i):
nginx复制# 只会匹配 /About,不会匹配 /about
rewrite ^/About /about.html last;
# 大小写都匹配
rewrite (?i)^/about /about.html last;
我见过不少配置,规则写 rewrite ^/blog/(.*)$ /blog.php?id=$1 last;,有用户访问 /BLOG/123 时直接404,排查半天才反应过来是大小写问题。所以如果你的业务场景难以控制用户输入的URL大小写,要么用 (?i),要么在location匹配上做文章。
另一个容易忽略的问题是,rewrite的replacement可以是一个完整的URL(带协议、域名),也可以是站内路径。当replacement是完整URL时,默认会执行302跳转,也就是客户端地址栏会变化;当replacement是站内路径时,默认行为是内部改写URI,客户端无感知。想改成显式的跳转,就要用flag。
2.2 last与break:纸面区别和真实差异
last 和 break 是内部改写时最常用的两个flag,也是出现误用最多的两个flag。
从官方定义上讲:
last:停止当前这组rewrite指令的执行,然后用改写后的URI重新搜索locationbreak:停止当前这组rewrite指令的执行,但不重新搜索location,继续在当前location里执行后续指令
从直觉上看,这两个flag的差异就是"要不要重新匹配location"。但实际配置中,这个差异会引发截然不同的结果。
举一个经典场景。假设你的站点使用PHP框架,入口文件是 index.php,你想让所有请求都交给入口处理:
nginx复制location / {
rewrite ^/(.*)$ /index.php?path=$1 last;
}
这里必须用 last。因为rewrite把 /foo 改写成了 /index.php,你期望Nginx重新匹配location,找到处理PHP的location(比如 location ~ \.php$),然后把请求交给PHP-FPM处理。如果这里用了 break,Nginx就不会重新匹配location了,而是在当前 location / 里继续往下执行。对静态文件请求来说这可能没问题,但对动态请求来说,请求根本不会进入处理PHP的location,结果往往是下载或者404。
反过来,再看另一个场景。假设你在一个location里已经确定了要走反向代理,只是需要把URI里某个前缀去掉:
nginx复制location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://backend;
}
这里用 break 是合理的。因为我们并不希望rewrite之后的 /$1 重新去匹配location,我们希望它就在当前location里继续执行,改写后的URI作为传给后端的内容。如果这里用了 last,改写后 /$1 会重新找location,很可能找不到合适的location,然后落到默认的 location /,行为直接失控。
我见过一个线上事故,就是有人在 location / 里写 rewrite ^/(.*)$ /index.php?path=$1 last;,恰好另一个location里也有rewrite并且互相匹配,结果两个location之间形成循环,Nginx报500并打出一堆rewrite or internal redirection cycle的日志。这类循环错误,十有八九是last滥用导致的。
2.3 redirect与permanent:临时跳转和永久跳转怎么选
redirect 是302临时重定向,permanent 是301永久重定向。选错的影响不在于技术层面,而在于缓存和SEO层面。
301会被浏览器和搜索引擎长期缓存。这意味着,一旦你发布过301跳转,即使后来把这个规则删了,很多用户在很长一段时间内访问旧地址时,浏览器还是会直接使用本地缓存的跳转结果,压根不会请求你的服务器。这给"改错了想撤销"带来了极大的麻烦。
我在开发测试阶段的原则是:一律用302或者干脆用rewrite的内部改写,线上确认不回头了再用301。 具体到配置里,如果使用return,return 302 和 return 301 同理。
还有一个很容易被忽视的场景:域名迁移或者URL结构大变时,如果你不确定规则以后还会不会调整,先用302顶上去,等流量转移完成、确认没有问题之后,再切换成301。这样做的代价是多跑一段时间临时跳转,但换来了随时回退的余地,值得。
3. 正则捕获与变量:写伪静态规则的高频细节
3.1 $1的顺序陷阱与贪婪匹配
rewrite通过括号 () 捕获正则匹配的部分,在replacement里用 $1、$2 依次引用。括号顺序按左括号出现的先后顺序编号,这个大多数人知道。但实际写规则时,有两个细节值得特意提一下。
第一个是贪婪匹配。正则默认是贪婪的,(.*) 会尽可能多地匹配。比如:
nginx复制rewrite ^/user/(.*)/(\d+)$ /user.php?name=$1&id=$2 last;
如果请求是 /user/zhang/123,看起来没问题,$1=zhang,$2=123。但如果请求是 /user/zhang/123/456,贪婪匹配会让 $1 变成 zhang/123,$2 变成 456。这不一定是你想要的,防止这种问题的方法是尽量让正则更精确,比如把中间部分写成 ([^/]+):
nginx复制rewrite ^/user/([^/]+)/(\d+)$ /user.php?name=$1&id=$2 last;
这样 [^/]+ 不会跨过斜杠,匹配行为更可控。
第二个是捕获组尽量少用 .*。捕获的目的是在replacement里引用,如果你并不需要在replacement里使用捕获内容,就不要加括号。多一个括号不仅多一次性能开销,还可能让 $1、$2 的对应关系变得难以阅读。
3.2 问号结尾:操作query string的两个小动作
rewrite处理query string的行为,我认为是文档里写得很轻、但实际使用频率很高的一处。默认情况下,rewrite改写URI之后,原来的query string会原样保留。所以:
nginx复制rewrite ^/old/(.*)$ /new/$1 last;
请求 /old/abc?page=2 会被改写成 /new/abc?page=2,page=2 被保留了。
如果你不想要旧的query string,只需要在replacement的末尾加上一个 ?:
nginx复制rewrite ^/old/(.*)$ /new/$1? last;
这个 ? 的作用是"清空query string"。请求 /old/abc?page=2 会被改成 /new/abc,没有参数。
但注意,如果你的replacement里本身带了参数,比如:
nginx复制rewrite ^/old/(.*)$ /new/$1?from=old permanent;
那么旧的query string会被 from=old 完全替换掉,page=2 不会保留。这个行为对SEO的URL参数整理特别有用,比如你想把一堆带跟踪参数的地址统一清洗成干净地址,这就是一个很实用的写法。
3.3 变量拼接与$request_uri的迷惑性
replacement里可以用Nginx变量,这个灵活性很高,但也最容易写出行为异常的规则。一个典型的混淆点是 $request_uri 和rewrite匹配到的URI不是一回事。
$request_uri是原始请求的完整URI,包含query string,且不做规范化- rewrite的pattern匹配的是规范化后的URI,不包含query string
所以,如果在replacement里用了 $request_uri,你实际上把query string也带进去了。这有时候是你想要的,有时候则会导致重复参数。
举个例子,你想把旧地址 http://example.com/index.php?id=123 全部收敛到首页 http://example.com/:
nginx复制rewrite ^/index\.php$ /home permanent;
这样写,?id=123 会被原样保留,变成 /home?id=123。如果你想去掉参数,需要写:
nginx复制rewrite ^/index\.php$ /home? permanent;
如果你不想写死 /home,而是想保留当前Host和路径、只改掉文件名,可以考虑变量的组合。比如:
nginx复制rewrite ^/index\.php$ $scheme://$host/? permanent;
这里 $scheme://$host 拼出完整地址,末尾带 ? 清空query string。这种写法在域名迁移、入口文件改名时很实用。不过需要提醒的是,变量拼接的正则可读性会下降,上线前务必用 nginx -t 检查,并且对实际URL做多组测试。
4. 四个高频场景的完整配置:从伪静态到HTTPS跳转
4.1 伪静态改写:把动态参数变成漂亮URL
伪静态大概是rewrite使用场景里占比最高的。需求通常是把 /article.php?id=123 这种动态地址变成 /article/123.html 这种看起来像静态页的URL。
配置其实很简单:
nginx复制location / {
rewrite ^/article/(\d+)\.html$ /article.php?id=$1 last;
}
请求 /article/123.html 时,正则捕获到 123,rewrite为 /article.php?id=123,然后 last 让Nginx重新匹配location,进入处理PHP的location,交给PHP-FPM执行。
如果你的站点是WordPress这类PHP框架,更推荐的做法其实是 try_files 而不是rewrite:
nginx复制location / {
try_files $uri $uri/ /index.php?$args;
}
try_files 的逻辑是:先检查 $uri 对应的文件是否存在,再检查目录是否存在,都不存在才最终重写到 /index.php?$args。它的好处是可以直接命中真实存在的静态资源,不会把所有请求都经过一次正则改写;而rewrite则是无条件执行正则改写。两者都能实现伪静态,但 try_files 在静态资源命中率高的站点上更可控,也不容易误伤。
4.2 域名规范化与301跳转
域名规范化通常是记录在案的三个需求:不带www跳转到www、带www跳转不带www、旧域名跳转新域名。
以不带www跳转www为例:
nginx复制server {
listen 80;
server_name example.com;
return 301 http://www.example.com$request_uri;
}
server {
listen 80;
server_name www.example.com;
# 正常业务配置
}
这里我用的是 return 而不是 rewrite,原因后面会展开。$request_uri 会保留原始请求的路径和query string,比如请求 http://example.com/foo?id=1,会302跳转到 http://www.example.com/foo?id=1,参数不会丢。
如果你非要用rewrite实现同样效果,可以这样写:
nginx复制rewrite ^(.*)$ http://www.example.com$1 permanent;
注意这里 $1 是匹配到的路径部分,query string默认会保留。这个写法其实也能用,但 return 的执行更直接,不会走正则引擎,也比rewrite少一层内部处理,所以我更推荐用 return 来处理纯跳转类需求。
4.3 HTTP强制跳转HTTPS
往HTTPS迁移的站点,通常会在80端口把所有请求301到443端口。最稳的写法是:
nginx复制server {
listen 80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
这个配置清晰地把"纯HTTP服务"和"HTTPS服务"分成了两个server块。HTTP块只负责跳转,HTTPS块负责真正的业务逻辑。这样做的好处是,你在HTTPS块里不会再误写一套跳转逻辑,从而避免重定向循环。
有人会写出这样的配置:
nginx复制server {
listen 443 ssl;
server_name example.com;
if ($scheme = http) {
return 301 https://example.com$request_uri;
}
}
这个 if 在443端口的server块里基本不会生效,因为请求到这个server时已经走的是HTTPS协议了,$scheme 永远是 https。而且如果在80端口的server块里用同样的 if 写法,虽然能工作,但远不如直接 return 301 直观。能用显式server块隔离的场景,尽量别用if判断协议。
4.4 根据User-Agent分流:PC和手机访问不同页面
移动端适配项目里,经常需要根据User-Agent把访问者导到不同的域名或路径。一个常见写法:
nginx复制location /news/ {
if ($http_user_agent ~* "(Mobile|Android|iPhone|iPad)") {
rewrite ^/news/(.*)$ http://m.example.com/news/$1 redirect;
}
}
这里用 ~* 做大小写不敏感的正则匹配,匹配到移动端UA后,rewrite到移动站地址,flag用 redirect 而不是 permanent。原因是UA规则可能需要频繁调整,如果用了301,客户端和搜索引擎会把跳转结果缓存很久,后续想改规则,影响很难撤销。
同一类需求,也可以用map变量提前把移动端判断抽离出来,让配置更清晰。不过map变量的细节后面在第5章和替代方案部分再展开。
这套if + rewrite的写法属于rewrite模块的安全用法,在正式环境里使用是比较常见的。真正需要避开的,是在if块里放其他模块的指令,这个下一章仔细说。
5. if is evil:条件重写的边界与替代写法
5.1 官方说if evil,evil在哪里
Nginx官方文档和社区wiki都反复警告过一件事:if 指令在location里是"evil"的。这句话被很多人引用,但理解得五花八门。其实官方的意思非常具体:在location里使用if时,if内部只能可靠地执行rewrite模块的指令(return、rewrite),如果放入其他指令(比如proxy_pass、try_files、access_log),行为可能不符合直觉,甚至产生难以排查的错误。
为什么?因为if是rewrite模块的指令,它是在rewrite阶段执行的。而rewrite阶段的位置在location匹配之后、content阶段之前。很多指令本来应该在content阶段或者更靠后的阶段执行,但把它们放在if里时,Nginx会对它们做一种特殊处理,导致它们的行为和预期不同。
官方文档给过一个经典例子:在location里写if判断文件是否存在,然后try_files,可能会出现死循环。还有人喜欢在if里写proxy_pass做灰度分流,结果发现有的请求走到了完全错误的upstream。
5.2 if里的安全用法和不安全用法
结合官方建议和我自己的实践,可以总结成一张表:
| if块内指令 | 安全性 | 说明 |
|---|---|---|
return |
安全 | 推荐,用于条件响应和条件跳转 |
rewrite |
安全 | 推荐,用于条件改写URI |
proxy_pass |
危险 | 可能出现不可预期的upstream选择问题 |
try_files |
危险 | 官方明确警告可能产生死循环 |
access_log |
危险 | 可能在错误阶段被处理 |
set |
大体安全 | 在rewrite阶段设置变量,配合map使用没有问题 |
所以我的原则是:if里只用return和rewrite,其他需求一律换方案。 如果你需要根据条件做反向代理分流,不要用if,用map配合upstream或者多个server/location来实现;如果你需要根据条件做try_files,也应该通过rewrite或map来处理。
一个安全的if示例,比如禁止特定请求方法:
nginx复制location /api/ {
if ($request_method = POST) {
return 405;
}
# 正常处理逻辑
}
这个写法安全、直观,是if正当使用的代表。
5.3 替代方案:map、return条件与try_files
很多if场景其实都能被其他机制替代。这里说三个最常见的。
第一个是 map。如果你需要根据某个变量的值来决定跳转到哪里,map可以在http块里预先算好目标地址,之后用return跳转:
nginx复制map $http_user_agent $mobile_target {
default http://www.example.com;
~*Mobile|Android|iPhone http://m.example.com;
}
server {
listen 80;
if ($http_user_agent ~* "Mobile|Android|iPhone") {
return 302 $mobile_target$request_uri;
}
}
其实在这个例子里,if仍然可以换成map和return的配合,但map本身的优势在于可以集中管理复杂的分支映射,让主配置保持精简。
第二个是 return 的条件判断。return本身可以配合多个变量条件,不一定非要写if。比如:
nginx复制set $block_flag 0;
if ($request_method = DELETE) {
set $block_flag 1;
}
if ($arg_debug = 1) {
set $block_flag 1;
}
if ($block_flag = 1) {
return 403;
}
这是社区里常见的"多个条件组合"技巧,用set变量记录判断结果,最后统一return。这比在每个if里直接写return可维护性更好,也属于安全用法。
第三个是 try_files。很多伪静态需求其实不需要rewrite,try_files 本身就够了。比如把不存在的文件统一交给出入口:
nginx复制location / {
try_files $uri $uri/ /index.php?$args;
}
这个方案避免了写复杂的正则,也减少了误伤概率。能用try_files解决的,我一般不会优先写rewrite。
6. 反向代理场景:rewrite与proxy_pass的配合方式
6.1 proxy_pass带不带URI的区别
热搜词里有"nginx反向代理",这里必须单独开一章,因为rewrite和proxy_pass配合时的路径规则,是我见过社区提问最多、误解最深的一块。
proxy_pass的配置可以分为两类:带URI和不带URI。
- 不带URI:
proxy_pass http://backend; - 带URI:
proxy_pass http://backend/;或proxy_pass http://backend/v2/;
当proxy_pass不带URI时,Nginx会把当前请求的原始URI原样传给后端。比如 location /api/ 里配了不带URI的proxy_pass,请求 /api/user,后端收到的是 /api/user。
当proxy_pass带URI时,规则就变了:location匹配到的URI部分会被proxy_pass里的URI替换掉。比如:
nginx复制location /api/ {
proxy_pass http://backend/v2/;
}
请求 /api/user,后端收到的是 /v2/user。因为location /api/ 匹配到的前缀 /api/ 被替换成了 /v2/。
不理解这个替换规则,是很多代理路径问题的根源。很多人会在这个场景里绕一大圈用rewrite去改路径,其实proxy_pass自身的URI替换能力已经能做到。
6.2 rewrite + proxy_pass:一个业务常见的API路径改造
虽然proxy_pass带URI能解决大部分路径替换问题,但有些场景仍然需要rewrite介入。典型场景是:后端接口本来带版本号,前端希望隐藏版本号,或者旧路径迁移到新路径。
假设后端接口是 /V1/products,但前端希望暴露成 /api/products,并且不想让前端感知版本号。配置可以这样:
nginx复制location /api/ {
rewrite ^/api/(.*)$ /V1/$1 break;
proxy_pass http://backend;
}
请求 /api/products 时,rewrite把它改为 /V1/products,然后用 break 停止后续rewrite,不重新匹配location,继续在当前location里执行proxy_pass。由于proxy_pass不带URI,后端收到的就是改写后的 /V1/products。
这里的 break 是关键。如果误写成 last,改写后的 /V1/products 会重新匹配location,可能导致请求走向完全不同的处理逻辑。这也是本章最核心的避坑点。
另一种等效写法是用带URI的proxy_pass:
nginx复制location /api/ {
proxy_pass http://backend/V1/;
}
请求 /api/products,location前缀 /api/ 被替换成 /V1/,后端收到 /V1/products。这两者效果相同,但后者少了一次rewrite正则执行,更简洁。我的建议是:能靠proxy_pass的URI替换解决的,就别上rewrite;只有需要更复杂的路径变换时,再用rewrite + break。
6.3 绕开rewrite:location嵌套与正则location方案
如果你不想用rewrite,还有一些纯粹的location机制可以实现路径改造。比如用正则location:
nginx复制location ~ ^/api/(.*)$ {
proxy_pass http://backend/V1/$1;
}
正则location里,$1 捕获 /api/ 后面的部分,拼接到proxy_pass的URI里。这种写法也能实现 /api/products 到 /V1/products 的映射。但注意,正则location里的proxy_pass不能带URI的说法需要细分:实际上像 http://backend/V1/$1 这种带变量的写法是被允许的,而 proxy_pass http://backend/V1/ 这种不带变量的写法在正则location里可能不会被正确处理。我建议想要稳妥就用rewrite + break或map,正则location配合proxy_pass变量的写法虽然能工作,但阅读成本高,团队维护起来也容易出错。
另外,location 本身是支持嵌套复杂逻辑的,比如把需要代理的路径单独拆成一个location,把需要改写的逻辑放在更外层的location。这本质上是把rewrite的执行范围缩小到更精确的上下文,是推荐的做法。
7. 线上调试三板斧与性能避坑
7.1 rewrite_log:打开Nginx的rewrite"行车记录仪"
遇到rewrite行为不对时,第一步不是盯着配置反复看,而是打开rewrite日志。在nginx.conf里开启:
nginx复制rewrite_log on;
error_log /var/log/nginx/error.log notice;
然后reload Nginx。之后访问触发rewrite的URL,在error.log里会看到类似这样的输出:
code复制[notice] 1234#0: *567890 "^/article/(\d+)\.html$" matches "/article/123.html", client: 1.2.3.4
[notice] 1234#0: *567890 rewritten uri: "/article.php?id=123"
这些日志会明确告诉你:正则是否匹配、匹配到了什么URI、rewrite成了什么URI。先用日志确认"规则到底执行没有",再来判断是正则问题还是flag问题。我排查rewrite问题的经验里,80%的情况靠这两行日志就能定位方向。
7.2 curl与error.log的配合定位
curl是测试rewrite最直接的工具。我常用的几个命令:
bash复制# 查看跳转目标(不跟随跳转)
curl -I "http://example.com/article/123.html"
# 查看完整状态码和耗时
curl -s -o /dev/null -w "%{http_code}\n" "http://example.com/article/123.html"
# 自定义Host测试(本地调试多站点时很有用)
curl -I -H "Host: example.com" "http://127.0.0.1/article/123.html"
用 curl -I 时,如果规则是302或301跳转,响应头里会有 Location 字段,直接看到跳转目标,非常直观。如果是内部改写,状态码是200,则需要配合rewrite_log一起看,确认rewrite之后的URI是什么。
本地调试多server块时,我会直接改curl的Host头来模拟不同域名,不需要真的把域名解析到本机,效率高很多。
当然,curl的返回结果是"服务器当前配置"的结果,浏览器可能因为本地缓存了301导致表现不同。所以测试301跳转规则时,建议用 curl -I 观察,而不是用浏览器反复刷新。
7.3 性能层面的三个建议
rewrite虽然方便,但它不是免费的。每次rewrite执行,Nginx都要跑一遍正则引擎。对于高并发站点来说,无节制的rewrite会带来不小的CPU开销。这里有三个建议:
第一个建议:尽量把rewrite放在更精确的location里,不要写在server块里让每个请求都跑一遍。 比如只有 /article/ 前缀的请求才需要伪静态,那就写在 location /article/ 里面,而不是写在server块里。
第二个建议:正则要写得收敛,不要用过长的、过度复杂的pattern。 ^/article/(\d+)\.html$ 比 ^/.*article.*\.html$ 不仅行为更可控,性能也更好。正则里的 .* 和嵌套分组都会增加回溯成本,能锚定就锚定。
第三个建议:能用return或try_files解决的,不要引入rewrite。 return执行完直接返回响应,连正则引擎都不用;try_files按顺序查文件,命中就结束。rewrite适合URI改写场景,但纯跳转、纯静态资源处理,都有比它更轻的方案。我在生产里见过有人写了一堆rewrite去实现301跳转,还特意用了flag,其实一句 return 301 就能解决,而且更不容易出错。
最后一个实际经验:配置完成后,不要只测一个正常URL就收工。多测几个边界URL,比如 /article/0.html、/article/abc.html、带query string的地址、大小写不同的地址。这些边界往往能暴露正则漏洞和flag误用。等到线上出问题再补测试,代价就大了。
