Go HTTP Header 的那些坑
透過簡單範例說明 Go 開發時常見的 HTTP Header 操作誤區
文章目錄
最近開發時常需要針對 HTTP Header 進行操作,過程中發現一些平常並不會特別注意但會造成問題的地方。
以下會以範例講解這陣子所遇到的坑(笑),文中範例程式可以到此 Github Repo 下載。
首先請各位看下面的範例程式,並猜看看使用者會收到哪種 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 避免問題發生。
這個範例需要執行兩個程式: 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)
}
Header1
與 Header2
之間的差別在於,Proxy 分別利用 Add
、Set
來修改 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 的值會得到什麼結果呢?
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 的值。
以下的 V1
與 V2
分別會回傳什麼值呢?
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
即可。
- https://godoc.org/net/http#Header
- https://github.com/fission/fission/pull/1029
- https://github.com/fission/fission/pull/1032
相關文章
- Go defer 遇上 os.Exit 時失效
- From NodeJs, Scala to Go
- Fission - Serverless Framework on Kubernetes
- 打造最小 Go Docker Image
文章內容的轉載、重製、發佈,請註明出處: https://tachingchen.com/tw/
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Pinterest
Email