bigywater.tech
微服务 · 30

路径重写:rewrite 与 proxy_pass 末尾斜杠的区别

proxy_pass 末尾多一个斜杠,上游收到的路径会差多少?rewrite 的 last、break 到底怎么走?%2F、.. 和 // 经过网关会变成什么?这篇用回显路径的上游把它们逐格量出来,也纠正了一个我自己预先的想法。

/v1users
location /api/ 配 proxy_pass http://u/v1(末尾没斜杠)时,/api/users 被转发成的路径
/x/y
location /api(没有末尾斜杠)配 proxy_pass http://u/ 时,/apix/y 被转发成的路径(它也被匹配了)
/a/b
带 URI 的 proxy_pass 把 /api/a%2Fb 转发成的路径(不带 URI 时原样是 /api/a%2Fb)

proxy_pass 的斜杠

实测 · 本机执行输出
  配置                                       | /api/users?id=1 | /api/ | /api                     | /apix/y | /api//users
  -----------------------------------------+-----------------+-------+--------------------------+---------+------------
  location /api/ + proxy_pass http://u     | /api/users?id=1 | /api/ | 301 → http://x:PORT/api/ | 404     | /api//users
  location /api/ + proxy_pass http://u/    | /users?id=1     | /     | 301 → http://x:PORT/api/ | 404     | /users     
  location /api/ + proxy_pass http://u/v1/ | /v1/users?id=1  | /v1/  | 301 → http://x:PORT/api/ | 404     | /v1/users  
  location /api + proxy_pass http://u/     | /users?id=1     | /     | /                        | /x/y    | /users     
  location /api + proxy_pass http://u/v1   | /v1/users?id=1  | /v1/  | /v1                      | /v1x/y  | /v1/users  
  location /api/ + proxy_pass http://u/v1  | /v1users?id=1   | /v1   | 301 → http://x:PORT/api/ | 404     | /v1users   
  看格子:proxy_pass 带 URI 时,用它替换请求路径里匹配 location 的那段前缀,其余部分接在后面,并且路径被规范化(连续斜杠合并);不带 URI 时原样转发整条路径(/api//users 原样到上游)。location /api(没有末尾斜杠)会把 /apix/y 也匹配进来,前缀 /api 被换掉后剩下的 x 接在 URI 后面(/x/y);location /api/ 配 proxy_pass http://u/v1(末尾没有斜杠)时,/api/users 变成 /v1users,前缀 /api/ 换成了 /v1,没有补斜杠。/api(不带斜杠)在 location /api/ 的配置下,nginx 自己回 301 补斜杠;/apix/y 不匹配 location /api/,落到没有 location 的默认处理(404)

带 URI 时替换匹配的前缀;不带 URI 时原样转发;末尾有没有斜杠差别很大。

rewrite 的标志

实测 · 本机执行输出
  标志        | 请求 /old/a/b?x=1 的结果(上游收到的路径,或 nginx 自己的响应)
  ----------+-------------------------------------------
  (无标志)     | hit /new/ location                        
  break     | /new/a/b?x=1                              
  last      | hit /new/ location                        
  redirect  | 302 → http://x:PORT/new/a/b?x=1           
  permanent | 301 → http://x:PORT/new/a/b?x=1           
  没有标志时,rewrite 改了 URI 之后,当前 location 里剩下的 rewrite 模块指令(rewrite、return、set、if)继续执行,然后 nginx 用改写后的 URI 重新选 location——效果和 last 一样,所以请求命中 /new/,根本没到上游(我原以为会接着执行 proxy_pass,实测不是);只有 break 才会停在当前 location,用改写后的 URI 去 proxy_pass(上游收到 /new/a/b?x=1);redirect / permanent 返回 302 / 301 让 client 重新请求。查询串默认保留

没有标志时改了 URI 就重新选 location(像 last);break 才会继续 proxy_pass。

路径的规范化

实测 · 本机执行输出
  写法                                                      | /api/a%20b?q=%41 | /api/a%2Fb | /api/x/../y | /api//z
  --------------------------------------------------------+------------------+------------+-------------+--------
  (a) location /api/ + proxy_pass http://u                | /api/a%20b?q=%41 | /api/a%2Fb | /api/x/../y | /api//z
  (b) location /api/ + proxy_pass http://u/               | /a%20b?q=%41     | /a/b       | /y          | /z     
  (c) rewrite ^/api/(.*)$ /$1 break + proxy_pass http://u | /a%20b?q=%41     | /a/b       | /y          | /z     
  不带 URI 的 proxy_pass 原样转发请求里的路径(%2F、.. 段、// 都保持原样);带 URI 的 proxy_pass 和 rewrite 之后转发的是 nginx 处理过的路径:.. 被解析掉、// 被合并、%2F 被解码成了真正的斜杠(/api/a%2Fb 变成 /a/b),%20 仍保持编码。网关和上游对路径的理解不一致(例如上游看到的是 %2F 还是 /),是路径穿越和缓存投毒类问题的来源之一

带 URI 的 proxy_pass 与 rewrite 之后会解码 %2F、解析 .. 和 //。

分步演示

路径重写
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。nginx 是 Homebrew 的 1.31.6;上游是回显请求路径的 Python 服务;请求用原始 socket 发出;我在跑实验前的预期有两处与实测不符:(1) 以为 location /api 配 proxy_pass http://u/ 会产生 //users,实测是 /users;(2) 以为 rewrite 没有标志时会接着执行 proxy_pass,实测等同 last。文章按实测写
    • 没有验证:变量出现在 proxy_pass 里时的 URI 处理规则;正则 location 里 proxy_pass 不能带 URI 的限制;merge_slashes off 的效果;更多的编码字符(%00、%5C、UTF-8);其它网关(Envoy、Traefik)的路径规范化选项
    京ICP备2026067502号-1京公网安备11010502063487号