問題描述
客戶端發起的HTTP POST請求, 到達服務器后請求方法莫名其妙變成了GET請求, 導致客戶端收到的是404。
問題定位
首先檢查代碼, 再三確認並且在測試環境上驗證后確保代碼沒問題。
因為是生產環境出現的問題, 便排查測試環境和生產環境的區別:
兩者客戶端代碼相同
兩者服務端代碼相同
生產環境用Nginx做了域名轉發, 而測試環境直接使用的IP訪問, 未使用代理
於是嘗試將問題定位在Nginx轉發上, 馬上重現問題, 並且觀察Nginx日志, 發現轉發時出現了如下日志:
[27/Jul/2020:15:53:01 +0800] [訪問的URL] "POST /api/v/game/query HTTP/1.1" 301 185 "-" "PostmanRuntime/7.25.0" "-"
[27/Jul/2020:15:53:01 +0800] [訪問的URL] "GET /api/v/game/query HTTP/1.1" 404 18 "http://訪問的URL/api/v/game/query" "PostmanRuntime/7.25.0" "-"
從上面的日志可以看出:
Nginx收到的請求確實為POST請求, 客戶端請求沒有問題。
從Nginx到服務端請求發生了變化: POST被轉換成了GET請求, 由此服務端返回404。
由此可以確定: 問題的出現是因為Nginx的轉發使HTTP方法類型發生了變化。
問題原因
那么Nginx為什么會把POST請求轉換成GET請求呢?注意上面的第一行日志中有301的字樣, 301狀態碼的意思是: 資源位置永久改變, 需要重定向, 通常用於將HTTP請求遷移到HTTPS。
到這里, 回頭看看Nginx的配置文件, 文件中配置了listen 443 ssl, ssl_certificate, ssl_certificate_key等參數, 即Nginx配置的是HTTPS服務, 所有請求將以HTTPS訪問, 對於HTTP請求, 將會被以HTTPS的形式重定向。
再看看客戶端發起請求的URL, 確實是HTTP請求, 所以觸發了重定向, 也就導致了問題的產生。
即使通過Nginx將HTTP轉換成了HTTPS, 這里也並沒有解釋為什么POST會變成GET請求, 這里就需要祭出著名的《圖解HTTP》中關於狀態碼的解釋了:
書中關於3xx狀態碼的解釋:
1. 301-Moved Permanently(永久性重定向), 該狀態碼表示請求的資源已經被分配了新的URI, 以后應使用資源現在所指的URI, 也就是說如果已經把資源對應的URI保存為書簽了, 這時應該按Location首部字段提示的URI重新保存。
2. 302-Found(臨時重定向), 該狀態碼表示請求的資源已經被分配了新的URI, 希望用戶(本次)能使用新的URI訪問。和301不同的是, 302不是永久移動, 只是臨時性質的, 也就是已移動的資源對應的URI將來還有可能發生改變, 如果URI被保存為書簽, 用戶不需要更新書簽。
3. 303-See Other(存在另一個URI), 該狀態碼表示請求的資源存在着另一個URI, 應使用GET方法定向獲取請求的資源。303和302功能相同, 但303明確表示客戶端應當采用GET方法獲取資源。比如, 當使用POST方法訪問時, 其執行后的處理結果是希望客戶端能以GET方法重定向到另一個URI上去, 則返回303狀態碼。
4. 304-Not Modified(未滿足條件的URI), 該狀態碼表示客戶端發送附帶條件的請求時, 服務器允許請求訪問資源, 如果未滿足條件, 則返回304。
5. 307-Temporary Redirect(臨時重定向), 該狀態碼與302有相同的意義, 302禁止POST變換成GET, 但是在實際使用中, 大家並不遵循, 仍然將POST轉換成了GET。307會遵照標准, 不會從POST變成GET。
注: 當301、302、303狀態碼返回時, 幾乎所有的瀏覽器都會把POST改成GET, 並刪除請求報文內的主體, 之后請求會自動再次發送。即使301, 302禁止將POST方法改成GET方法, 但實際使用中大家仍然將其改成了GET。
到這里, 原因已經很明了了。
問題解決
對於這里的問題場景, 我們不希望POST請求被改成GET請求, 則解決方法有:
如果可以, 將客戶端發起的HTTP請求改為HTTPS請求, 這樣便不會重定向。
將Nginx配置文件中的return 301 $URI永久重定向改為return 307 $URI臨時重定向。
