经典题解析:Nginx 会合并 URL 双斜线吗,代理时又会发生什么

图中原始请求目标与规范化路径分开记录,代理转发还要看 proxy_pass 是否带有 URI 部分。

客户端发送 /api//v1,后端收到的却可能是 /v1,也可能仍是 /api//v1。问题不只是 Nginx 会不会合并斜线,而是匹配、重写和转发分别使用了哪一种路径表示。只看一条配置里有没有多写一个斜线,很容易把几个阶段混在一起。

原始目标和当前路径不是同一个变量

$request_uri 表示原始请求 URI,包含参数;$uri 表示当前规范化的 URI,不包含查询参数,并可能在内部重定向等处理后改变。记录它们时,应把“原样收到什么”和“当前按什么路径处理”分开。

自设一个请求目标 /api//v1?x=1。在相应默认合并行为且没有其他重写的简化条件下,可以把原始目标记作完整字符串,而用于匹配的规范化路径是 /api/v1。查询参数 x=1 不会因为路径变量没有显示它,就自动从整个请求里消失。

规范化也不只涉及双斜线,还包括相应的转义解码和路径处理。若上游和代理对相同输入采取不同规则,同一串字节可能被理解成不同资源。因此要明确表示,而不是把它们统称成一个“URL”。

merge_slashes 影响合并与匹配

merge_slashes 默认开启,用于把相邻多个斜线压成一个。这会影响前缀和正则 location 等匹配。关闭它之后,//scripts/... 不一定再与原先的 /scripts/ 路径规则以同样方式匹配,不能只把它看成展示样式变化。

有些业务可能确实需要保留某类原始路径信息,但是否关闭,应结合完整路由与上游协议判断。为了让一条请求看起来原样透传就全局关掉,可能同时改变其他入口的处理范围。先检查各阶段语义,再决定如何配置。

还要注意虚拟服务器选择与配置生效范围,不能在一个测试块里修改后,就假定所有请求都受同一取值影响。最终应以实际加载配置和请求路径为准。

原始路径、匹配路径、上游路径分开看

带 URI 的 proxy_pass 会做替换

考虑普通前缀 location /api/。如果 proxy_pass 指向 http://backend/,其中结尾的 / 是 URI 部分,那么转发时会用这个 URI 替换规范化请求路径中与 location 匹配的部分。

在前面 /api//v1 的简化例子里,匹配按规范化后的 /api/v1 进行,替换 /api/ 后,得到的上游路径是 /v1。不能把这个行为说成“删掉代理前缀后保留所有原始字符”,因为被替换的对象已经是规范化路径。

如果目标还带其他路径,例如 /service/,替换结果又会不同。这里讲的是明确的前缀 location 示例,不能把替换公式直接套到正则 location、命名 location 或所有变量形式上。

不带 URI 时也有条件

如果 proxy_pass 只写 http://backend,不附带 URI,处理原始请求时可以按客户端发送的形式传递请求目标;但若处理过程中 URI 已被改变,转发规则又会涉及完整的规范化新 URI。

因此,在没有重写的本例里,上游可能仍看到 /api//v1,这与代理内部曾用 /api/v1 做匹配并不矛盾。一个表示负责当前路由,另一个表示可以用于原始转发,不能从“内部合并过”直接推出“上游一定收到合并结果”。

带变量的 proxy_pass、rewrite 改变路径等还具有各自规则,诊断时要把这些条件列出来。只比较两个配置末尾有没有斜线,却忽略路径已经被改写,可能得出相反结论。

用同一组输入核对两端

测试可以记录原始请求目标、当前路径、命中的 location,以及上游实际收到的路径。分别覆盖单斜线、双斜线、查询参数和相关转义输入,并确认测试客户端没有提前替你规范化请求。

如果只在脚本中模拟前缀替换,它只能检查该示例的字符串规则,不是实际 Nginx 请求解析验证。真实部署还需要观察实际服务两端日志,尤其不能用一条简化模拟证明所有 URI 都处理安全。

路径一致性还关系到访问控制与缓存等组件是否指向同一资源。设计上应让各阶段使用的身份规则相互兼容,而不是依靠某个阶段“看起来等价”就忽略后续差异。

若访问规则按 /api/v1 生效,而上游把 /api//v1 当成另一条路径,你会怎样确认这两端实际保护的是同一个资源?