Goのhttp.Headerについて
2016-12-15 / Go
この記事は Go (その3) Advent Calendar 2016 の15日目の記事です。
GoでHTTPのHeaderを扱う際に使用する http.Header
は map[string][]string
を拡張している。なので、直接キーにヘッダー名を使用して値を操作することができる。
また Get
や Set
などアクセサを介してもデータを取得することができる。たぶんアクセサを使用することが多いと思う。
例えば、以下のようなことができる。
import (
"fmt"
"net/http"
)
func main() {
h := http.Header{}
h.Set("User-Agent", "Dummy")
fmt.Println(h.Get("User-Agent"))
fmt.Println(h["User-Agent"])
fmt.Println(h.Get("user-agent"))
fmt.Println(h["user-agent"])
fmt.Println(h.Get("USER-AGENT"))
fmt.Println(h["USER-AGENT"])
}
出力結果は、
Dummy
[Dummy]
Dummy
[]
Dummy
[]
となる。 Get
を使用するとヘッダー名の大文字小文字に関係なく値が取得できる。
なぜだろう?気になったので調べてみた。
アクセサを使用するとキー名が正規化される
http.Header
のアクセサでは net/textproto
パッケージの MIMEHeader
が使用(キャスト)されていて、そのアクセサを呼ぶようになっている。
// Set sets the header entries associated with key to
// the single element value. It replaces any existing
// values associated with key.
func (h Header) Set(key, value string) {
textproto.MIMEHeader(h).Set(key, value)
}
// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns "".
// To access multiple values of a key, access the map directly
// with CanonicalHeaderKey.
func (h Header) Get(key string) string {
return textproto.MIMEHeader(h).Get(key)
}
textproto.MIMEHeader
のアクセサでは、 textproto.CanonicalMIMEHeaderKey
が使用されている。
// Set sets the header entries associated with key to
// the single element value. It replaces any existing
// values associated with key.
func (h MIMEHeader) Set(key, value string) {
h[CanonicalMIMEHeaderKey(key)] = []string{value}
}
// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns "".
// Get is a convenience method. For more complex queries,
// access the map directly.
func (h MIMEHeader) Get(key string) string {
if h == nil {
return ""
}
v := h[CanonicalMIMEHeaderKey(key)]
if len(v) == 0 {
return ""
}
return v[0]
}
この textproto.CanonicalMIMEHeaderKey
が、MIME Headerのルールに合うようにヘッダー名を変更してくれている。
// CanonicalMIMEHeaderKey returns the canonical format of the
// MIME header key s. 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".
// MIME header keys are assumed to be ASCII only.
// If s contains a space or invalid header field bytes, it is
// returned without modifications.
(実装は省略)
そういうことで、 http.Header
のアクセサを使うと、ヘッダー名の大文字小文字を気にせず値を取得することができるようになっている。
なお net/http
パッケージにも CanonicalHeaderKey
という textproto.CanonicalMIMEHeaderKey
をラップした関数が用意されている。
ヘッダー情報を map[string]string で扱いたいとき
以下のようなJSONをUnmarshalしたい場合、
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
...
},
"body": "{\r\n\t\"a\": 1\r\n}"
}
次のような構造体では上手く行かない。http.Header
の実体は map[string][]string
だから。
type Request struct {
Headers http.Header `json:"headers"`
Body json.RawMessage `json:"body"`
}
map[string]string
だと当然だが上手く行く。
type Request struct {
Headers map[string]string `json:"headers"`
Body json.RawMessage `json:"body"`
}
マップの値を取得する場合には、キーに対して CanonicalHeaderKey
を使う。
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
b := []byte(`{
"headers": {
"Accept-Encoding": "gzip, deflate"
},
"body": "{\r\n\t\"a\": 1\r\n}"
}`)
var req struct {
Headers map[string]string `json:"headers"`
Body json.RawMessage `json:"body"`
}
json.Unmarshal(b, &req)
key := http.CanonicalHeaderKey("accept-encoding")
fmt.Println(req.Headers[key])
// gzip, deflate と出力される
}
https://play.golang.org/p/QkMCHmx36M
ヘッダー名が正しく送信されてくるとは限らない
上記の例で、送信されてくるヘッダー名が正規化されていれば良いが、そうじゃないこともある。
Configure Proxy Integration for a Proxy Resource - Amazon API Gateway に載っているJSONでは…
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"cache-control": "no-cache",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Content-Type": "application/json",
"headerName": "headerValue",
"Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com",
"Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f",
"User-Agent": "PostmanRuntime/2.4.5",
"Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==",
"X-Forwarded-For": "54.240.196.186, 54.182.214.83",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
cache-control
とか headerName
とかある。これは単なるtypoかもしれないが、正しい情報が常に送られてくるとは言い切れない。
うまくデータを扱うためには、 json.Unmarshaler
に対応して、 json.Unmarshal
の際にヘッダー名を正規化するしかなさそう。textproto.MIMEHeader
を参考に、次のような型を定義してみた。
type Header map[string]string
func (h *Header) UnmarshalJSON(bytes []byte) error {
var m map[string]string
err := json.Unmarshal(bytes, &m)
if err != nil {
return err
}
for k, v := range m {
m[http.CanonicalHeaderKey(k)] = v
}
*h = m
return nil
}
func (h Header) Get(key string) string {
if h == nil {
return ""
}
return h[http.CanonicalHeaderKey(key)]
}
取得する箇所はこんな感じになる。
fmt.Println(req.Headers.Get("accept-encoding"))
fmt.Println(req.Headers.Get("Cache-Control"))
https://play.golang.org/p/25cUIvAkCy
無事にヘッダー名の大文字小文字を気にせずに、値が取得できるようになった。
まとめ
- HTTP のヘッダーを扱う
http.Header
を使用する際にはアクセサを使用する - 独自にヘッダー名を扱う必要がある場合は、
http.CanonicalHeaderKey
を活用する