Http 請求頭中的 Proxy-Connection

發表於2015-12-14

文章目錄

平時用 Chrome 開發者工具抓包時,經常會見到 Proxy-Connection 這個請求頭。之前一直沒去了解什麼情況下會產生它,也沒去了解它有什麼含義。最近看完《HTTP 權威指南》第四章「連線管理」和第六章「代理」之後,終於搞明白了這是因為給瀏覽器設定了代理(Proxy)。而神器 Fiddler 的抓包原理就是讓瀏覽器請求走它開的本地代理,所以開了 Fiddler 必然會產生這個請求頭。

代理改變了什麼?

為了徹底弄清這個問題,我們先來看下設定瀏覽器代理之後,HTTP 請求報文有那些變化。下面分別是設定代理前後訪問同一 URL 的請求報文(省略了無關內容):

設定代理之後,瀏覽器連線的是代理伺服器,不再是目標伺服器,這個變化單純從請求報文中無法看出。請求報文中的變化有兩點:第一行中的 request-URL 變成了完整路徑;Connection 請求頭被替換成了 Proxy-Connection。我們分別來看這兩個變化。

為什麼需要完整路徑?

早期的 HTTP 設計中,瀏覽器直接與單個伺服器進行對話,不存在虛擬主機。單個伺服器總是知道自己的主機名和對應埠,為了避免冗餘,瀏覽器只需要傳送主機名之外的那部分 URI 就行了。代理出現之後,部分 URI 徹底杯具,代理伺服器無法得知使用者想要訪問的URI在什麼主機上。為此,HTTP/1.0 要求瀏覽器為代理請求傳送完整的 URI,也就是說規範告訴瀏覽器的實現者必須這麼做。

顯式地給瀏覽器配置代理後,瀏覽器會為之後的請求使用完整 URI,解決了代理無法定位資源的問題。但是代理可以出現在連線的任何位置,很多代理對瀏覽器來說不可見,如反向代理或路由器代理。所以實際上,幾乎所有的瀏覽器都會為每個請求加上內容為主機名的 HOST 請求頭,來徹底解決虛擬主機問題。對於 HTTP/1.1 請求,HOST 請求頭必須存在,否則會收到 400 錯誤;對於 HTTP/1.0 請求,如果連線的是代理伺服器,使用相對 URI,並且沒有 HOST 請求頭,會發生錯誤。

Proxy-Connection 是什麼?

HTTP 中的 Connection,用來對 HTTP 連線進行說明,多個說明使用英文逗號隔開,如:

其中,「my-header」是本次請求中其它 Header 的名字(不區分大小寫),表示這個 Header 只與當前連線有關。實際上,Connection 本身也只有當前連線有關。當客戶端和服務端存在一個或多箇中間實體(如代理)時,每個請求報文都會從客戶端(通常是瀏覽器)開始,逐跳發給伺服器;伺服器的響應報文,也會逐跳返回給客戶端。通常,即使通過了重重代理,請求頭都會原封不動的發給伺服器,響應頭也會原樣被客戶端收到。但 Connection,以及 Connection 定義的其它 Header,只是對上個節點和當前節點之間的連線進行說明,必須在報文轉給下個節點之前刪除,否則可能會引發後面要提到的問題。其它不能傳遞的 Header 還有Prxoy-Authenticate、Proxy-Connection、Transfer-Encoding 和 Upgrade。

「close」表示操作完成後需要關閉當前連線;Connection 還允許任何字串作為它的值,如「my-connection」,用來存放自定義的連線說明。HTTP/1.0 預設不支援持久連線,很多 HTTP/1.0 的瀏覽器和伺服器使用「Keep-Alive」這個自定義說明來協商持久連線:瀏覽器在請求頭裡加上 Connection: Keep-Alive,服務端返回同樣的內容,這個連線就會被保持供後續使用。對於 HTTP/1.1,Connection: Keep-Alive 已經失去意義了,因為 HTTP/1.1 除了顯式地將 Connection 指定為 close,預設都是持久連線。

有了上面的背景知識,我們來看問題。網際網路上,存在著大量簡陋並過時的代理伺服器在繼續工作,它們很可能無法理解 Connection——無論是請求報文還是響應報文中的 Connection。而代理伺服器在遇到不認識的 Header 時,往往都會選擇繼續轉發。大部分情況下這樣做是對的,很多使用 HTTP 協議的應用軟體擴充套件了 HTTP 頭部,如果代理不傳輸擴充套件欄位,這些軟體將無法工作。

如果瀏覽器對這樣的代理髮送了 Connection: Keep-Alive,那麼結果會變得很複雜。這個 Header 會被不理解它的代理原封不動的轉給服務端,如果伺服器也不能理解就還好,能理解就徹底杯具了。伺服器並不知道 Keep-Alive 是由代理錯誤地轉發而來,它會認為代理希望建立持久連線,服務端同意之後也返回一個 Keep-Alive。同樣,響應中的 Keep-Alive 也會被代理原樣返給瀏覽器,同時代理還會傻等伺服器關閉連線——實際上,服務端已經按照 Keep-Alive 指示保持了連線,即使資料回傳完成,也不會關閉連線。另一方面,瀏覽器收到 Keep-Alive 之後,會複用之前的連線傳送剩下的請求,但代理不認為這個連線上還會有其他請求,請求被忽略。這樣,瀏覽器會一直處於掛起狀態,直到連線超時。

這個問題最根本的原因是代理伺服器轉發了禁止轉發的 Header。但是要升級所有老舊的代理也不是件簡單的事,所以瀏覽器廠商和代理實現者協商了一個變通的方案:首先,顯式給瀏覽器設定代理後,瀏覽器會把請求頭中的 Connection 替換為 Proxy-Connetion。這樣,對於老舊的代理,它不認識這個 Header,會繼續發給伺服器,伺服器也不認識,代理和伺服器之間不會建立持久連線(不能正確處理 Connection 的都是 HTTP/1.0 代理),伺服器不返回 Keep-Alive,代理和瀏覽器之間也不會建立持久連線。而對於新代理,它可以理解 Proxy-Connetion,會用 Connection 取代無意義的 Proxy-Connection,並將其傳送給伺服器,以收到預期的效果。

顯然,如果瀏覽器並不知道連線中有老舊代理的存在,或者在老舊代理任意一側有新代理的情況下,這種方案仍然無濟於事。所以有時候伺服器也會選擇徹底忽略 HTTP/1.0 的 Keep-Alive 特性:對於 HTTP/1.0 請求,從不使用持久連線,也從不返回 Keep-Alive。

最後

通過上面的內容可以看到,瀏覽器對代理請求頭的修改,都是為了儘可能的相容網路中各種不規範的中轉裝置,使網路更健壯。

最後再提一句,用 Fiddler 和其它工具檢視同一個請求頭,會發現 Fiddler 顯示的是 Connection,而其它工具顯示的是 Proxy-Connection。這是因為大部分情況下,Fiddler 會把 Proxy-Connection 換回 Connection 來顯示,只是展現上的差別而已。

相關文章