1. 1. 配置如图
  2. 2. 测试一下
  3. 3. 探究
  4. 4. 解决
    1. 4.0.1. Nginx 配置文件
    2. 4.0.2. OpenResty

当 proxy_pass 遇上变量时

最近开发 Api Gateway 时遇上一个有趣的现象,但由于传统使用 Nginx 时很少会通过变量来进行分流故踩到这个坑。

配置如图

该规则表示匹配到 / 时将流量导向 http://japi/v1/ , 转换成 Nginx 的配置如下:

1
2
3
location / {
proxy_pass http://japi/v1/
}

测试一下

手动配置下 Nginx 的规则,将流量分发到我用 NC 开启的一个伪 WebServer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#: Request
curl "http://localhost:9999/?abc=123"

#: Nginx log
127.0.0.1 [12/Dec/2017:22:43:38 +0800] "GET /?abc=123 HTTP/1.1" 200 67 "-" "curl/7.54.0" 0.000 0.000 -

#: WebServer
GET /v1/?abc=123 HTTP/1.0
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: http
Host: localhost
Connection: close
User-Agent: curl/7.54.0
Accept: */*

由此看来,配置还是OK的, 但模仿下 Gateway 的场景,将配置改成这样?

判断是否OK的依据为该请求是否符合预期,当 Nginx 转发请求时,在本例中符合预期的请求应该包含了完整的 URL 和 QueryString , 从前面的结果来看,常规的用法并不会出现这样的问题,但如果是通过变量来动态化,就会出现如下的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#: Nginx Config
server {
listen 9999;
location / {
set $upstream_url "http://japi/v1/";
proxy_pass $upstream_url;
}
access_log /data/logs/tt.log access;
}

#: Nginx Log
127.0.0.1 [12/Dec/2017:22:49:55 +0800] "GET /?abc=123 HTTP/1.1" 200 67 "-" "curl/7.54.0" 0.000 0.000 -

#: Request
curl "http://localhost:9999/?abc=123"

#: WebServer
GET /v1/ HTTP/1.0
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: http
Host: localhost
Connection: close
User-Agent: curl/7.54.0
Accept: */*

此时的请求就与预期不符了, QueryString 似乎被丢弃了

探究

打开 Nginx 的源码( http/modules/ngx_http_proxy_module.c )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* the request line */

b->last = ngx_copy(b->last, method.data, method.len);
*b->last++ = ' ';

u->uri.data = b->last;

// proxy_lengths 属性,只会在 proxy_pass 中使用变量的情况下才会设置。
// 这里的 uri 指端口到参数中的一段,也就是请求首行 /?abc=123 这一部分
if (plcf->proxy_lengths && ctx->vars.uri.len) {
b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);

} else if (unparsed_uri) {
// 这部分处理 HTTP 0.9 的
b->last = ngx_copy(b->last, r->unparsed_uri.data, r->unparsed_uri.len);

} else {
// 如果 proxy_pass 用的是字面量,会连同参数一起拷贝进来
if (r->valid_location) {
b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);
}

if (escape) {
ngx_escape_uri(b->last, r->uri.data + loc_len,
r->uri.len - loc_len, NGX_ESCAPE_URI);
b->last += r->uri.len - loc_len + escape;

} else {
b->last = ngx_copy(b->last, r->uri.data + loc_len,
r->uri.len - loc_len);
}

if (r->args.len > 0) {
*b->last++ = '?';
b->last = ngx_copy(b->last, r->args.data, r->args.len);
}
}

文档 prxy_pass 有明确提到当 proxy_pass 指令使用变量时将忽略原来的 URI, 注意 (URIURL 的区别)。

A special case is using variables in the proxy_pass statement: The requested URL is not used and you are fully responsible to construct the target URL yourself.

解决

当弄明白其中缘由后,其实就很好处理了

Nginx 配置文件

手动构造其 URI 即可

1
set $upstream_url "http://tt/v1/$is_args$args";

OpenResty

1
2
3
4
5
6
7
8
9
local args = ngx_encode_args(ngx_req.get_uri_args()) 
if #args > 0 then
local args_found, _, _ = ngx_re.find(ngx_var.upstream_url, "\\?", "jo")
if args_found == 0 or utils.is_null(args_found) then
ngx_var.upstream_url = ngx_var.upstream_url.. '?' .. args
else
ngx_var.upstream_url = ngx_var.upstream_url.. '&' .. args
end
end