Go HTTP Header 的那些坑

透過簡單範例說明 Go 開發時常見的 HTTP Header 操作誤區

ta-ching chen

3 minute read

 文章目錄

前言 

最近開發時常需要針對 HTTP Header 進行操作,過程中發現一些平常並不會特別注意但會造成問題的地方。

以下會以範例講解這陣子所遇到的坑(笑),文中範例程式可以到此 Github Repo 下載。

ResponseWriter.Write 導致客戶端收到錯誤 HTTP 狀態碼 

首先請各位看下面的範例程式,並猜看看使用者會收到哪種 HTTP 狀態碼?

http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("foobar"))
    w.WriteHeader(http.StatusBadRequest)
})

覺得是 400 Bad Request? 其實正確答案是 200 OK。

$ curl -i http://127.0.0.1:8080
HTTP/1.1 200 OK
Date: Thu, 20 Dec 2018 06:31:33 GMT
Content-Length: 6
Content-Type: text/plain; charset=utf-8

foobar

並且 Server 出現以下的 log

2018/12/20 14:31:33 http: multiple response.WriteHeader calls

造成問題的原因在於 WriteHeader 與 Write 的呼叫順序。官方文件註解內有針對這個問題作出解釋:

If WriteHeader is not called explicitly, the first call to Write will trigger an implicit WriteHeader(http.StatusOK).

意思就是指,若 Write 先呼叫時會自動預設狀態碼為 http.StatusOK,因此後續再呼叫 WriteHeader 時就會變成多重寫入狀態碼的問題發生,導致客戶端收到不正確的狀態。

因此若伺服器需要回傳錯誤碼時需要先呼叫 WriteHeader 設定錯誤碼後,再呼叫 Write 寫入 body 避免問題發生。

Header.Set 跟 Header.Add 相似卻不一樣 

這個範例需要執行兩個程式: server.go 與 proxy.go。

作用是當請求送到 Proxy 時,它會將在請求的 Header 加上部分 metadata 後轉送到後方 Server 處理。

http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    h1 := r.Header["Header1"]
    h2 := r.Header["Header2"]
    msg := fmt.Sprintf("header1: %v, header2: %v", h1, h2)
    w.Write([]byte(msg))
})
func (p Proxy) handle(w http.ResponseWriter, r *http.Request) {
    // We may want to attach metadata to request header
    r.Header.Add("Header1", "bar")
    r.Header.Set("Header2", "foo")
    p.proxy.ServeHTTP(w, r)
}

Header1Header2 之間的差別在於,Proxy 分別利用 AddSet 來修改 Header。

一樣請各位思考兩者的輸出結果會有什麼差異呢?

$ curl -i http://127.0.0.1:8080 -H 'header1: 123' -H 'header2: 456'
HTTP/1.1 200 OK
Content-Length: 34
Content-Type: text/plain; charset=utf-8
Date: Thu, 20 Dec 2018 06:53:54 GMT

header1: [123 bar], header2: [foo]

答案是 Header1 回傳的陣列內包含兩個值而 Header2 只有一個。魔鬼就藏在細節裡(X) 答案就藏在官方文件裡(O)

Add adds the key, value pair to the header. It appends to any existing values associated with key.

Set sets the header entries associated with key to the single element value. It replaces any existing values associated with key.

因此要特別注意後方伺服器的使用狀況來選擇正確的函示。

注意 Header.Get 回傳值 

接續上面的狀況,假設改用 Header.Get 來取得 Header 的值會得到什麼結果呢?

r.Header.Get("Header1")

答案是得到 123 而不是 [123, bar]

func (h Header) Get(key string) string {
    return textproto.MIMEHeader(h).Get(key)
}

func (h MIMEHeader) Get(key string) string {
    if h == nil {
        return ""
    }
    v := h[CanonicalMIMEHeaderKey(key)]
    if len(v) == 0 {
        return ""
    }
    return v[0]
}

深入到標準庫實作時可以發現他只會回傳第一個值,因此若想要取得整個 Header 值的話就需要直接存取 Header 的值。

直接存取 Header 要注意 Key 大小寫 

以下的 V1V2 分別會回傳什麼值呢?

http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    v1 := r.Header["foo"]
    v2 := r.Header[textproto.CanonicalMIMEHeaderKey("foo")]

    msg := fmt.Sprintf("V1: %v, V2: %v", v1, v2)

    w.Write([]byte(msg))
})

答案是 V2 才能正確取到值。

$ curl -i http://127.0.0.1:8080 -H 'foo: 123'
HTTP/1.1 200 OK
Date: Thu, 20 Dec 2018 07:14:07 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8
  
V1: [], V2: [123]

原因在於任何請求 Header 進到 go http server 後會被強制轉換成大小寫敏感的 canonical key。

The canonicalization converts the first letter and any letter following a hyphen to upper case; the rest are converted to lowercase. For example, the canonical key for “accept-encoding” is “Accept-Encoding”.

因此可以在存取 Header map 值之前,先呼叫 textproto.CanonicalMIMEHeaderKey 進行轉換以確保取得正確值。

但值得注意的是,其實 Header.Get 在呼叫時便會進行轉換。因此若非特別需要的話其實使用 Header.Get 即可。

參考連結 

相關文章

comments powered by Disqus