type
Post
status
Published
date
Jan 18, 2023
slug
golang-header-set
summary
HTTP header 中的 Get, Set, Add 你真的会用吗
tags
开发
Golang
category
技术分享
icon
password

Header.Add() or Header.Set()

假设我们的服务在接收到请求后,想在 header 中加入一些自定义的内容再转发给另一个服务,此时会怎么做呢?
一般来说会写出下面这样的代码
// proxy.go func (p Proxy) handle(w http.ResponseWriter, r *http.Request) { // Header1 用 Add 设置 r.Header.Add("Header1", "hello") // Header2 用 Set 设置 r.Header.Set("Header2", "world") // 转发到 server 去处理 p.proxy.ServeHTTP(w, r) } // server.go 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)) })
当我们发起如下请求时,请求体中预设好 header1 和 header2 的值,此时会打印出什么呢
$ curl -i http://127.0.0.1:8080 -H 'header1: 123' -H 'header2: 321' HTTP/1.1 200 OK Content-Length: 34 Content-Type: text/plain; charset=utf-8 Date: Tue, 17 Jan 2023 09:11:45 GMT header1: [123 hello], header2: [world]
没错,header1 中有两个值,分别是请求时已有的 123 和我们在转发时加入的 hello
而 header2 中只有一个值,是在转发时加入的 world
 
造成这个现象的魔鬼就在细节里,官方文档如是说
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 中加入新的 key 时一定要选用合适的方法

Header.Get()

接着上面的例子,如果此时我们用 Header.Get() 来获取 header 的值会发生什么呢
r.Header.Get("Header1")
答案是 123 而不是 [123, hello]
 
查看 Golang 标准库源码可以发现,Get 方法的实现只会返回匹配到的 key 的第一个值,所以要获取 key 对应的全部值还是要直接从 header 里读取,在使用时一定要注意
// Get gets the first value associated with the given key. If // there are no values associated with the key, Get returns "". // It is case insensitive; textproto.CanonicalMIMEHeaderKey is // used to canonicalize the provided key. Get assumes that all // keys are stored in canonical form. To use non-canonical keys, // access the map directly. 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 时使用如下姿势会发生什么呢?
// server.go http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) { v1 := r.Header["hello"] v2 := r.Header[textproto.CanonicalMIMEHeaderKey("hello")] msg := fmt.Sprintf("V1: %v, V2: %v", v1, v2) w.Write([]byte(msg)) })
答案是只有 v2 可以正确取到值
$ curl -i http://127.0.0.1:8080 -H 'hello: 123' HTTP/1.1 200 OK Date: Tue, 17 Jan 2023 09:20:08 GMT Content-Length: 17 Content-Type: text/plain; charset=utf-8 V1: [], V2: [123]
造成这个现象的原因是:任何请求进入到 Go HTTP Server 后,header 会被强制转换成大小写敏感的 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 中的 hello: 123 在进入 HTTP后,hello 会被转换为 Hello,此时再用 hello 直接获取是无法取到正确值的,调用 textproto.CanonicalMIMEHeaderKey 方法进行转换后就可以正确取值
 
但是:要注意的是,调用 Header.Get 时会对传入的参数进行隐式转换,如果此时是用
r.Header.Get("hello")
进行取值,则可以直接获取到正确的值而无需手动转换
 

参考链接

 
Golang 那些事:HTTP Header 的那些坑(1)函数式编程:代码简洁之道?