Nginx 變量漫談(六)
Nginx 內建變量用在“子請求”的上下文中時,其行為也會變得有些微妙。
前面在 (三) 中我們已經知道,許多內建變量都不是簡單的“存放值的容器”,它們一般會通過注冊“存取處理程序”來表現得與眾不同,而它們即使有存放值的容器,也只是用于緩存“存取處理程序”的計算結果。我們之前討論過的 $args 變量正是通過它的“取處理程序”來返回當前請求的 URL 參數串。因為當前請求也可以是“子請求”,所以在“子請求”中讀取 $args,其“取處理程序”會很自然地返回當前“子請求”的參數串。我們來看這樣的一個例子:
location /main {
echo "main args: $args";
echo_location /sub "a=1&b=2";
}
location /sub {
echo "sub args: $args";
}
這里在 /main
接口中,先用 echo 指令輸出當前請求的 $args 變量的值,接著再用 echo_location 指令發起子請求 /sub
. 這里值得注意的是,我們在 echo_location 語句中除了通過第一個參數指定“子請求”的 URI 之外,還提供了第二個參數,用以指定該“子請求”的 URL 參數串(即 a=1&b=2
)。最后我們定義了 /sub
接口,在里面輸出了一下 $args 的值。請求 /main
接口的結果如下:
$ curl 'http://localhost:8080/main?c=3'
main args: c=3
sub args: a=1&b=2
顯然,當 $args 用在“主請求” /main
中時,輸出的就是“主請求”的 URL 參數串,c=3
;而當用在“子請求” /sub
中時,輸出的則是“子請求”的參數串,a=1&b=2
。這種行為正符合我們的直覺。
與 $args 類似,內建變量 $uri 用在“子請求”中時,其“取處理程序”也會正確返回當前“子請求”解析過的 URI:
location /main {
echo "main uri: $uri";
echo_location /sub;
}
location /sub {
echo "sub uri: $uri";
}
請求 /main
的結果是
$ curl 'http://localhost:8080/main'
main uri: /main
sub uri: /sub
這依然是我們所期望的。
但不幸的是,并非所有的內建變量都作用于當前請求。少數內建變量只作用于“主請求”,比如由標準模塊ngx_http_core 提供的內建變量 $request_method.
變量 $request_method 在讀取時,總是會得到“主請求”的請求方法,比如 GET
、POST
之類。我們來測試一下:
location /main {
echo "main method: $request_method";
echo_location /sub;
}
location /sub {
echo "sub method: $request_method";
}
在這個例子里,/main
和 /sub
接口都會分別輸出 $request_method 的值。同時,我們在 /main
接口里利用echo_location 指令發起一個到 /sub
接口的 GET
“子請求”。我們現在利用 curl
命令行工具來發起一個到/main
接口的 POST
請求:
$ curl --data hello 'http://localhost:8080/main'
main method: POST
sub method: POST
這里我們利用 curl
程序的 --data
選項,指定 hello
作為我們的請求體數據,同時 --data
選項會自動讓發送的請求使用 POST
請求方法。測試結果證明了我們先前的預言,$request_method 變量即使在 GET
“子請求” /sub
中使用,得到的值依然是“主請求” /main
的請求方法,POST
.
有的讀者可能覺得我們在這里下的結論有些草率,因為上例是先在“主請求”里讀取(并輸出)$request_method 變量,然后才發“子請求”的,所以這些讀者可能認為這并不能排除 $request_method在進入子請求之前就已經把第一次讀到的值給緩存住,從而影響到后續子請求中的輸出結果。不過,這樣的顧慮是多余的,因為我們前面在 (五) 中也特別提到過,緩存所依賴的變量的值容器,是與當前請求綁定的,而由ngx_echo 模塊發起的“子請求”都禁用了父子請求之間的變量共享,所以在上例中,$request_method 內建變量即使真的使用了值容器作為緩存(事實上它也沒有),它也不可能影響到 /sub
子請求。
為了進一步消除這部分讀者的疑慮,我們不妨稍微修改一下剛才那個例子,將 /main
接口輸出$request_method 變量的時間推遲到“子請求”執行完畢之后:
location /main {
echo_location /sub;
echo "main method: $request_method";
}
location /sub {
echo "sub method: $request_method";
}
讓我們重新測試一下:
$ curl --data hello 'http://localhost:8080/main'
sub method: POST
main method: POST
可以看到,再次以 POST
方法請求 /main
接口的結果與原先那個例子完全一致,除了父子請求的輸出順序顛倒了過來(因為我們在本例中交換了 /main
接口中那兩條輸出配置指令的先后次序)。
由此可見,我們并不能通過標準的 $request_method 變量取得“子請求”的請求方法。為了達到我們最初的目的,我們需要求助于第三方模塊 ngx_echo 提供的內建變量 $echo_request_method:
location /main {
echo "main method: $echo_request_method";
echo_location /sub;
}
location /sub {
echo "sub method: $echo_request_method";
}
此時的輸出終于是我們想要的了:
$ curl --data hello 'http://localhost:8080/main'
main method: POST
sub method: GET
我們看到,父子請求分別輸出了它們各自不同的請求方法,POST
和 GET
.
類似 $request_method,內建變量 $request_uri 一般也返回的是“主請求”未經解析過的 URL,畢竟“子請求”都是在 Nginx 內部發起的,并不存在所謂的“未解析的”原始形式。
如果真如前面那部分讀者所擔心的,內建變量的值緩存在共享變量的父子請求之間起了作用,這無疑是災難性的。我們前面在 (五) 中已經看到 ngx_auth_request 模塊發起的“子請求”是與其“父請求”共享一套變量的。下面是一個這樣的可怕例子:
map $uri $tag {
default 0;
/main 1;
/sub 2;
}
server {
listen 8080;
location /main {
auth_request /sub;
echo "main tag: $tag";
}
location /sub {
echo "sub tag: $tag";
}
}
這里我們使用久違了的 map 指令來把內建變量 $uri 的值映射到用戶變量 $tag
上。當 $uri 的值為 /main
時,則賦予 $tag
值 1,當 $uri 取值 /sub
時,則賦予 $tag
值 2,其他情況都賦 0
. 接著,我們在 /main
接口中先用 ngx_auth_request 模塊的 auth_request
指令發起到 /sub
接口的子請求,然后再輸出變量 $tag
的值。而在 /sub
接口中,我們直接輸出變量 $tag
. 猜猜看,如果我們訪問接口 /main
,將會得到什么樣的輸出呢?
$ curl 'http://localhost:8080/main'
main tag: 2
咦?我們不是分明把 /main
這個值映射到 1
上的么?為什么實際輸出的是 /sub
映射的結果 2
呢?
其實道理很簡單,因為我們的 $tag
變量在“子請求” /sub
中首先被讀取,于是在那里計算出了值 2
(因為 $uri 在那里取值 /sub
,而根據 map 映射規則,$tag
應當取值 2
),從此就被 $tag
的值容器給緩存住了。而 auth_request
發起的“子請求”又是與“父請求”共享一套變量的,于是當 Nginx 的執行流回到“父請求”輸出 $tag
變量的值時,Nginx 就直接返回緩存住的結果 2
了。這樣的結果確實太意外了。
從這個例子我們再次看到,父子請求間的變量共享,實在不是一個好主意。
(未完待續)
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成