Zwlin's Blog

HTTP-Trailer

2020/10/20

Concept

Trailer 是一个响应首部,允许发送方在分块发送的消息后面添加额外的元信息,这些元信息可能是随着消息主体的发送动态生成的,比如消息的完整性校验,消息的数字签名,或者消息经过处理之后的最终状态等。

Grammar

Trailer: header-names

header-names 是出现在分块信息挂载部分的消息首部。以下首部字段不允许出现:

Example

 1HTTP/1.1 200 OK 
 2Content-Type: text/plain 
 3Transfer-Encoding: chunked
 4Trailer: Expires
 5
 67\r\n 
 7Mozilla\r\n 
 89\r\n 
 9Developer\r\n 
107\r\n 
11Network\r\n 
120\r\n 
13Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n
14\r\n

解释:

  1. Header里面的Transfer-Encoding必须是chunked,也就是说不能指定Content-Length
  2. Trailer 的字段名字必须在 Header里面提前声明,比如上面的Trailer: Expires
  3. TrailerBody发完之后再发,格式和Header类似。

Implement in Go

用Go实现一个HTTP客户端,对所发的Body计算MD5并通过Trailer传给服务端。服务端收到请求并对Body进行校验。

Server Code

 1package main
 2
 3import (
 4	"crypto/md5"
 5	"fmt"
 6	"io/ioutil"
 7	"log"
 8	"net/http"
 9)
10
11func checkMd5(w http.ResponseWriter, req *http.Request) {
12	fmt.Printf("Header:%+v\n", req.Header)
13	fmt.Println("Trailer before read body:")
14	fmt.Println(req.Trailer)
15
16	data, err := ioutil.ReadAll(req.Body)
17	bodyMd5 := fmt.Sprintf("%x", md5.Sum(data))
18	defer req.Body.Close()
19
20	fmt.Println("body:", string(data))
21	fmt.Println("md5:", bodyMd5)
22	fmt.Println("error:", err)
23	fmt.Println("Trailer after read body:")
24	fmt.Println(req.Trailer)
25
26	if req.Trailer.Get("md5") != bodyMd5 {
27		panic("body md5 not equal")
28	}
29}
30
31func main() {
32	http.HandleFunc("/", checkMd5)
33	log.Fatal(http.ListenAndServe(":12345",nil))
34}

Client Code

 1package main
 2
 3import (
 4	"crypto/md5"
 5	"fmt"
 6	"hash"
 7	"io"
 8	"net/http"
 9	"os"
10	"strconv"
11	"strings"
12)
13
14type headerReader struct {
15	reader io.Reader
16	md5    hash.Hash
17	header http.Header
18}
19
20func (r *headerReader) Read(p []byte) (n int, err error) {
21	n, err = r.reader.Read(p)
22	if n > 0 {
23		r.md5.Write(p[:n])
24	}
25	if err == io.EOF {
26		r.header.Set("md5", fmt.Sprintf("%x", r.md5.Sum(nil)))
27	}
28	return
29}
30
31func main() {
32	h := &headerReader{
33		reader: strings.NewReader("body"),
34		md5:    md5.New(),
35		header: http.Header{"md5": nil, "size": []string{strconv.Itoa(len("body"))}},
36	}
37	req, err := http.NewRequest("POST", "http://localhost:12345", h)
38	if err != nil {
39		panic(err)
40	}
41	req.ContentLength = -1
42	req.Trailer = h.header
43	resp, err := http.DefaultClient.Do(req)
44	if err != nil {
45		panic(err)
46	}
47	fmt.Println(resp.Status)
48	_, err = io.Copy(os.Stdout, resp.Body)
49	if err != nil {
50		panic(err)
51	}
52}

Result

Server result

$ go run server.go
Header:map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
Trailer before read body:
map[Md5:[] Size:[]]
body: body
md5: 841a2d689ad86bd1611447453c22c6fc
error: <nil>
Trailer after read body:
map[Md5:[841a2d689ad86bd1611447453c22c6fc] Size:[4]]

Client result

$ go run client.go
200 OK

nc result

$ nc -l 12345
POST / HTTP/1.1
Host: localhost:12345
User-Agent: Go-http-client/1.1
Transfer-Encoding: chunked
Trailer: Md5,Size
Accept-Encoding: gzip

4
body
0
Md5: 841a2d689ad86bd1611447453c22c6fc
size: 4

Conclusion

可以看到服务端在读完body之前只能知道有Md5这个Trailer,值为空;读完body之后,能正常拿到TrailerMd5值。

Go语言使用Trailer也有几个注意事项:

  1. req.ContentLength 必须设置为0或者-1,这样body才会以chunked的形式传输。
  2. req.Trailer需要在发请求之前声明所有的key字段,在body发完之后设置相应的value,如果客户端提前知道Trailer的值的话也可以提前设置,比如上面例子里面的size字段。
  3. 发完body之后Trailer不允许再更改,否则可能会因为map并发读写,导致程序panic,同样的道理服务端在读body的时候也不应该对Trailer有引用。
  4. 服务端必须读完body之后才能知道Trailer的值。

References