掌握Rewrite重写技术提升项目路由灵活性
又出幺蛾子了,API路径重写不生效
今天上线前最后检查功能,发现前端调用的某个接口死活走不到后端。报404,但后端同学说服务根本没收到请求。我第一反应是:是不是拼错了?查了一遍URL,没问题。然后怀疑代理配置——我们用Nginx做了rewrite重写,把所有 /api/v2/ 开头的请求转发到另一个服务。可就是不生效。
最离谱的是,本地开发环境好好的,一上预发环境就挂。这就不是代码问题了,肯定是部署或配置差异。
折腾了半天发现,原来是location匹配顺序搞的鬼
先说结论吧,核心问题是Nginx的 location 匹配优先级导致rewrite规则压根没被执行。我一开始写的配置长这样:
location / {
proxy_pass http://frontend;
}
location /api/v2/ {
rewrite ^/api/v2/(.*)$ /v2/$1 break;
proxy_pass http://backend-service;
}
看起来没啥毛病对吧?但实际上,这个配置在某些情况下会被第一个 location / 拦下来,后面的就进不去。因为虽然正则匹配更精确,但Nginx里普通前缀匹配(比如 location /api/v2/)如果没有加修饰符,默认是按声明顺序来的,除非你明确加上 ^~ 或者用正则写法并提高优先级。
这里我踩了个坑:我以为只要路径匹配就会进入对应块,结果不是。Nginx的匹配规则比想象中复杂一点。后来翻文档才发现:带有 = 精确匹配最高,其次是 ^~ 前缀匹配(不继续正则),再然后是正则 ~ 和 ~*,最后才是普通的前缀匹配。
而上面那段配置里的两个都是普通前缀匹配,所以会按“最长匹配”原则选一个,但不会阻止后续正则检查——等等,其实还没完,如果后面还有正则表达式location,还会再比一次!这机制真够绕的。
后来试了下发现,加个 ^~ 就好了
改法很简单,在关键 location 前面加个 ^~,表示一旦匹配成功就不考虑正则了:
location / {
proxy_pass http://frontend;
}
location ^~ /api/v2/ {
rewrite ^/api/v2/(.*)$ /v2/$1 break;
proxy_pass http://backend-service;
}
这么一改,果然通了。请求进来,先匹配 /api/v2/user/info,命中第二个location,直接执行rewrite和proxy_pass,不再往下找任何正则location。完美。
不过这里注意我踩过好几次坑:break 和 last 的区别也得搞清楚。当时为了调试,我把 break 改成 last,结果rewrite之后又去重新匹配location,反而跳回了第一个 /,导致proxy到了错误的服务。
break:停止rewrite处理,使用当前结果直接proxylast:停止当前阶段,但发起新的location查找
所以这里必须用 break,不然 rewrite 后的路径会被再次路由,可能又掉进别的块里。
还有一种方案是用正则 + if,但我没敢用
网上有人建议这么写:
location / {
if ($request_uri ~* ^/api/v2/(.*)) {
rewrite ^/api/v2/(.*)$ /v2/$1 break;
proxy_pass http://backend-service;
}
proxy_pass http://frontend;
}
看着简洁,但亲测不可靠。首先 if 在location里本身就容易出问题,尤其是在复杂的header或querystring场景下。其次 proxy_pass 不能放在if里面当条件执行,Nginx会警告你这是不安全的操作,某些版本甚至直接报错。
而且这种嵌套写法维护起来太恶心了,以后别人来看一脸懵。所以我果断放弃这条路。
最终上线配置是这样的
我现在线上跑的完整配置如下,兼顾清晰和稳定性:
server {
listen 80;
server_name example.com;
# 静态资源缓存优化
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# API v2 路径重写并代理
location ^~ /api/v2/ {
rewrite ^/api/v2/(.*)$ /v2/$1 break;
proxy_pass http://backend-service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 兜底:其他所有请求交给前端服务
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
这里面几个关键点我都标出来了:
- 静态资源单独处理,避免干扰主流程
^~ /api/v2/强制优先匹配,防止被/拦截- rewrite用
break终止,保证不二次匹配 - proxy头信息补全,让后端能拿到真实IP等数据
这套现在跑了两天,日志上看没再出现404或者误转发的情况。算是稳住了。
顺手提一嘴:本地开发怎么模拟?
我们在本地用webpack devServer做代理,也得保持一致逻辑。对应的 vue.config.js 或 vite.config.ts 得配上类似规则:
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api/v2': {
target: 'http://localhost:9002',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/v2/, '/v2')
}
}
}
})
这里rewrite也是去掉前面那层/api/v2,换成/v2开头。跟Nginx那边逻辑对齐,避免本地正常、上线就崩的尴尬。
改完后还有一两个小问题,但无大碍
目前唯一的小瑕疵是:如果有带 query 参数的请求,比如 /api/v2/user?id=123,rewrite后参数自动保留了,没问题。但日志里看到个别请求出现了双斜杠,像 //v2/user。排查发现是因为原始路径结尾多了一个斜杠,rewrite后变成 /v2//user。不影响功能,HTTP服务器一般都能处理,就没动了。
要是强迫症受不了,可以在rewrite里加个替换:
rewrite ^/api/v2/(.*)$ /v2/$1 break;
rewrite ^(/v2/.*)//(.*)$ $1/$2 break; # 处理双斜杠
但我觉得没必要,反而增加复杂度。现在的方案不是最优的,但最简单,够用就行。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流
这类rewrite问题每次遇到都得重新翻一遍Nginx文档,尤其是location匹配顺序这块,真心希望官方能把这部分说明写得再直白点。实际项目中很多人都是复制粘贴改改就上线,等到出问题才来查,浪费时间。
顺便提醒大家:别迷信“看起来能跑”的配置,一定要理解背后的匹配机制。特别是上线前最好用curl多测几种路径,确认rewrite和proxy行为符合预期。
这个技巧的拓展用法还有很多,比如根据用户agent分流、按域名重定向、灰度发布路径改写等等,后续会继续分享这类实战记录。

暂无评论