最近出现了一个这样的问题
我启动了一个 nginx 的一个 docker 容器
然后 容器的端口为 80, 映射到宿主机 81 端口
后端服务有一个 sendRedirect("/xxx"), 然后 客户端这边 拿到的端口是 80
然后 导致客户端 访问不到, 本文的一些 知识是 衍生自这个问题
302 转发相关的流程
从前端页面 到 nginx, 转发了一个请求 "/api/HelloWorld/sendRedirect"
nginx 将请求转发给了 后台服务, "/HelloWorld/sendRedirect"
然后 这个后台服务 中有一个 sendRedirect("/HelloWorld/listFormWithoutHeader")
然后 由后台服务 将响应回复给 nginx "http://localhost:8080/HelloWorld/listFormWithoutHeader"
然后 nginx 将相应 回复给客户端 "http://localhost/api/HelloWorld/listFormWithoutHeader"
可以看到 这个过程中 location 这个请求头是有数次调整
第一层是 后端服务的处理, 发现 sendRediect 的请求不是绝对地址, 自动拼接上了 本域 的相关信息
第二层是 nginx 的处理, 去掉了 后端服务的域的相关信息, 拼接上了 本域的相关信息
我们这里 就来梳理一下 这里的整个流程
以下截图, 调试基于 nginx-1.18.0
docker-compose 映射端口, 宿主机的端口 和 容器中的端口不一致
- master:nginx jerry$ cat docker-compose.yml
- version: "2"
-
- services:
- nginx:
- container_name: nginx
- image: nginx:latest
- ports:
- - "81:80"
- volumes:
- - ./data:/etc/nginx
- # - ./html:/usr/share/nginx/html
- - /Users/jerry/WebstormProjects/HelloWorld:/usr/share/nginx/html
nginx 到 后端服务 的路由配置
- location ^~ /api/ {
- root html;
- index index.html index.htm;
- proxy_pass http://192.168.0.104:8080/;
- }
访问这个 sendRedirect, 结果发现 Location 之后的 url 端口不对, 导致访问不到

81 端口上面的 目标服务

读完此篇 希望你能够 明白问题之所在
"/HelloWorld/sendRedirect" 以及 "/HelloWorld/listFormWithoutHeader" 的相关业务代码
- /**
- * HelloWorldController
- *
- * @author Jerry.X.He <970655147@qq.com>
- * @version 1.0
- * @date 2022-02-27 21:21
- */
- @RestController
- @RequestMapping("/HelloWorld")
- public class HelloWorldController {
-
- @RequestMapping("/sendRedirect")
- public void sendRedirect(HttpServletResponse response) throws Exception {
- response.sendRedirect("/HelloWorld/listFormWithoutHeader");
- }
-
- @GetMapping("/listFormWithoutHeader")
- public List<JSONObject> listFormWithoutHeader(
- String param01, String param02
- ) {
- List<JSONObject> result = new ArrayList<>();
- result.add(wrapEntity("param01", param01));
- result.add(wrapEntity("param02", param02));
- userService.list();
- return result;
- }
-
- }
nginx 里面 location 的配置
- location ^~ /api/ {
- root html;
- index index.html index.htm;
- proxy_pass http://localhost:8080/;
- }
浏览器中访问 "/HelloWorld/sendRedirect" 结果如下
可以看到 "/HelloWorld/sendRedirect" 的响应是 302, 跳转到了这里的 "http://localhost/api/HelloWorld/listFormWithoutHeader"
页面上显示的 即为 "http://localhost/api/HelloWorld/listFormWithoutHeader" 的响应结果

找到这个 相对来说比较简单
如果 sendRediect 的 location 以 "//" 表示绝对路径, 直接拼接上 请求的协议返回
否则 拼接上本域的相关信息 成为完整的 url 返回

然后 外层设置 status 为 302, 以及配置 Location 请求头给客户端

我们先看一下 nginx 拿到 location 请求头之后的处理
可以看到的是 服务端响应的 "Location" 是 "http://localhost:8080/HelloWorld/listFormWithoutHeader"
但是 这里 ngx_http_upstream_process_headers 处理之后, 去掉了 上游服务器的 域的相关信息, 拼接上了 "/api/"前缀 和 "/HelloWorld/listFormWithoutHeader"
注意 这里的 locatoin 又成为了一个 相对路径, 所以 参照上面 tomcat 的做法, nginx 应该需要拼接上 当前域 的相关信息
- (gdb) b ngx_http_upstream.c:2432
- Note: breakpoint 1 also set at pc 0x10d5bedda.
- Breakpoint 2 at 0x10d5bedda: file src/http/ngx_http_upstream.c, line 2432.
- (gdb) c
- Continuing.
-
- Breakpoint 1, ngx_http_upstream_process_header (r=0x7f8f0100b850,
- u=0x7f8f0100cbe0) at src/http/ngx_http_upstream.c:2432
- 2432 if (ngx_http_upstream_process_headers(r, u) != NGX_OK) {
- (gdb) print r->headers_out.location
- $1 = (ngx_table_elt_t *) 0x0
- (gdb) next
- 2436 ngx_http_upstream_send_response(r, u);
- (gdb) print r->headers_out.location
- $2 = (ngx_table_elt_t *) 0x7f8f0100be00
- (gdb) print r->headers_out.location.value
- $3 = {len = 37, data = 0x7f8f0100d659 "/api/HelloWorld/listFormWithoutHeader"}
nginx 拼接上当前域的相关信息到 Location 涉及的相关代码

nginx 拼接上当前域的相关信息到 Location
- Breakpoint 3, ngx_http_header_filter (r=0x7f8f01826450)
- at src/http/ngx_http_header_filter_module.c:321
- 321 c = r->connection;
- (gdb) next
- 323 if (r->headers_out.location
- (gdb) next
- 324 && r->headers_out.location->value.len
- (gdb) next
- 325 && r->headers_out.location->value.data[0] == '/'
- (gdb) next
- 326 && clcf->absolute_redirect)
- (gdb) next
- 323 if (r->headers_out.location
- (gdb) next
- 328 r->headers_out.location->hash = 0;
- (gdb) next
- 330 if (clcf->server_name_in_redirect) {
- (gdb) next
- 335 host = r->headers_in.server;
- (gdb) next
- 337 } else {
- (gdb) next
- 346 port = ngx_inet_get_port(c->local_sockaddr);
- (gdb) next
- 349 + host.len
- (gdb) next
- 350 + r->headers_out.location->value.len + 2;
- (gdb) next
- 348 len += sizeof("Location: https://") - 1
- (gdb) next
- 352 if (clcf->port_in_redirect) {
- (gdb) print host
- $4 = {len = 9, data = 0x7f8f01800031 "localhost"}
- (gdb) print port
- $5 = 80
-
- (gdb) b ngx_http_header_filter_module.c:517
- Breakpoint 4 at 0x10d5ce56c: file src/http/ngx_http_header_filter_module.c, line 517.
- (gdb) c
- Continuing.
- Breakpoint 4, ngx_http_header_filter (r=0x7f8f01826450)
- at src/http/ngx_http_header_filter_module.c:517
- 517 if (host.data) {
- (gdb) print host
- $6 = {len = 9, data = 0x7f8f01800031 "localhost"}
- (gdb) next
- 519 p = b->last + sizeof("Location: ") - 1;
- (gdb) next
- 521 b->last = ngx_cpymem(b->last, "Location: http",
- (gdb) next
- 530 *b->last++ = ':'; *b->last++ = '/'; *b->last++ = '/';
- (gdb) print b->start
- $7 = (u_char *) 0x7f8f02804e20 "HTTP/1.1 302 \r\nServer: nginx/1.18.0\r\nDate: Sun, 26 Jun 2022 01:40:21 GMT\r\nContent-Length: 0\r\nLocation: http"
- (gdb) next
- 531 b->last = ngx_copy(b->last, host.data, host.len);
- (gdb) next
- 533 if (port) {
- (gdb) next
- 537 b->last = ngx_copy(b->last, r->headers_out.location->value.data,
- (gdb) next
- 542 r->headers_out.location->value.len = b->last - p;
- (gdb) print b->start
- $8 = (u_char *) 0x7f8f02804e20 "HTTP/1.1 302 \r\nServer: nginx/1.18.0\r\nDate: Sun, 26 Jun 2022 01:40:21 GMT\r\nContent-Length: 0\r\nLocation: http://localhost/api/HelloWorld/listFormWithoutHeader"
- (gdb) next
- 543 r->headers_out.location->value.data = p;
- (gdb) next
- 544 ngx_str_set(&r->headers_out.location->key, "Location");
- (gdb) next
- 546 *b->last++ = CR; *b->last++ = LF;
- (gdb) next
- 549 if (r->chunked) {
- (gdb) print r->headers_out.location.value
- $9 = {len = 53,
- data = 0x7f8f02804e87 "http://localhost/api/HelloWorld/listFormWithoutHeader\r\n"}
客户端拿到的 Location 响应头

这个过程和 nginx 拿到请求之后 处理 发送给上游服务器是一个相反的过程
这里是拿到 "http://localhost:8080/HelloWorld/listFormWithoutHeader" , 去掉 上游服务器的信息, 增加 "/api/" 前缀
nginx 拿到请求之后 处理 发送给上游服务器是 增加 上游服务器的信息, 去掉 "/api/" 前缀

调试信息
- Breakpoint 5, ngx_http_proxy_rewrite (r=0x7f8f04000450, h=0x7f8f04000a00,
- prefix=0, len=22, replacement=0x7ffee26c3cf0)
- at src/http/modules/ngx_http_proxy_module.c:2713
- 2713 new_len = replacement->len + h->value.len - len;
- (gdb) print h->value
- $17 = {len = 54,
- data = 0x7f8f04002259 "http://localhost:8080/HelloWorld/listFormWithoutHeader"}
- (gdb) print replacement
- $18 = (ngx_str_t *) 0x7ffee26c3cf0
- (gdb) print replacement.data
- $19 = (u_char *) 0x7f8f0181e9dc "/api/"
- (gdb) info locals
- p = 0x7fff718efafc <small_malloc_should_clear+284> "L\211\347I\211\304H\205\300\017\205%\004"
- data = 0x7ffee26c3d50 "\003"
- new_len = 8
- (gdb) print len
- $20 = 22
- (gdb) next
- 2715 if (replacement->len > len) {
- (gdb) next
- 2731 p = ngx_copy(h->value.data + prefix, replacement->data,
- (gdb) next
- 2734 ngx_memmove(p, h->value.data + prefix + len,
- (gdb) next
- 2738 h->value.len = new_len;
- (gdb) next
- 2740 return NGX_OK;
- (gdb) print h->value
- $21 = {len = 37, data = 0x7f8f04002259 "/api/HelloWorld/listFormWithoutHeader"}
完