Goのhttp.Headerについて

2016-12-15

この記事は Go (その3) Advent Calendar 2016 の15日目の記事です。

GoでHTTPのHeaderを扱う際に使用する http.Headermap[string][]string を拡張している。なので、直接キーにヘッダー名を使用して値を操作することができる。
また GetSet などアクセサを介してもデータを取得することができる。たぶんアクセサを使用することが多いと思う。

例えば、以下のようなことができる。

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

無事にヘッダー名の大文字小文字を気にせずに、値が取得できるようになった。

まとめ

Published: 2016-12-15  /  Share: X