Slide 1

Slide 1 text

HTTPステータスコードが 意図した値にならないとき 2022/08/03 Let’s Go Talk #2 5分LT

Slide 2

Slide 2 text

自己紹介 - aboy です - コネヒト株式会社 - 最近はママリの検索を最高にする仕事をしてます - あと最近GoをさわっていてTech VisionのGo戦略を推進中

Slide 3

Slide 3 text

このLTのモチベーション - GoでHTTPサーバーをつくった - テストを書いていて、HTTPステータスコードが意図した値にならない

Slide 4

Slide 4 text

例えばこんなコード type MyHandler struct{} func (h *MyHandler) Sample(w http.ResponseWriter, r *http.Request) { w.Write([]byte("sample")) w.WriteHeader(http.StatusTeapot) } func Test(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() h := &MyHandler{} h.Sample(res, req) if res.Code != http.StatusTeapot { t.Errorf("status code got %d, should be %d", res.Code, http.StatusTeapot) } } === RUN Test main_test.go:40: status code got 200, should be 418 --- FAIL: Test (0.00s)

Slide 5

Slide 5 text

http.ResponseWriterに何かありそうだ type MyHandler struct{} func (h *MyHandler) Sample(w http.ResponseWriter, r *http.Request) { w.Write([]byte("sample")) w.WriteHeader(http.StatusTeapot) } func Test(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() h := &MyHandler{} h.Sample(res, req) if res.Code != http.StatusTeapot { t.Errorf("status code got %d, should be %d", res.Code, http.StatusTeapot) } } === RUN Test main_test.go:40: status code got 200, should be 418 --- FAIL: Test (0.00s)

Slide 6

Slide 6 text

ResponseWriterのメソッドは呼ぶ順番に意味がある - https://pkg.go.dev/net/http#ResponseWriter - godocにインタフェースを実装するにあたって守らなければならないふるま いが書かれている - If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK) before writing the data. - Writeメソッドのgodocから抜粋

Slide 7

Slide 7 text

実装例)httptest.ResponseRecorder - httptestパッケージのResponseRecorderを読んでいく - ResponseRecorderはhttp.ResponseWriterを実装したもの

Slide 8

Slide 8 text

WriteHeader() - rw.wroteHeaderなら何もせずreturn // WriteHeader implements http.ResponseWriter. func (rw *ResponseRecorder) WriteHeader(code int) { if rw.wroteHeader { return } checkWriteHeaderCode(code) rw.Code = code rw.wroteHeader = true if rw.HeaderMap == nil { rw.HeaderMap = make(http.Header) } rw.snapHeader = rw.HeaderMap.Clone() }

Slide 9

Slide 9 text

WriteHeader() - rw.wroteHeaderに値を代入しているのはWriteHeader()内のみ // WriteHeader implements http.ResponseWriter. func (rw *ResponseRecorder) WriteHeader(code int) { if rw.wroteHeader { return } checkWriteHeaderCode(code) rw.Code = code rw.wroteHeader = true if rw.HeaderMap == nil { rw.HeaderMap = make(http.Header) } rw.snapHeader = rw.HeaderMap.Clone() }

Slide 10

Slide 10 text

WriteHeader() - つまりWriteHeader()では、ResponseRecorderのインスタンスは一度だけ HTTPステータスコードを設定できる // WriteHeader implements http.ResponseWriter. func (rw *ResponseRecorder) WriteHeader(code int) { if rw.wroteHeader { return } checkWriteHeaderCode(code) rw.Code = code rw.wroteHeader = true if rw.HeaderMap == nil { rw.HeaderMap = make(http.Header) } rw.snapHeader = rw.HeaderMap.Clone() }

Slide 11

Slide 11 text

Write() - 1行目でrw.writeHeader()という非公開メソッドを呼んでいるのでそいつを 見にいく // Write implements http.ResponseWriter. The data in buf is written to // rw.Body, if not nil. func (rw *ResponseRecorder) Write(buf []byte) (int, error) { rw.writeHeader(buf, "") if rw.Body != nil { rw.Body.Write(buf) } return len(buf), nil }

Slide 12

Slide 12 text

writeHeader() - 色々あるけど今回の目的に沿って読むなら気になる箇所が2つ func (rw *ResponseRecorder) writeHeader(b []byte, str string) { if rw.wroteHeader { return } if len(str) > 512 { str = str[:512] } m := rw.Header() _, hasType := m["Content-Type"] hasTE := m.Get("Transfer-Encoding") != "" if !hasType && !hasTE { if b == nil { b = []byte(str) } m.Set("Content-Type", http.DetectContentType(b)) } rw.WriteHeader(200) }

Slide 13

Slide 13 text

writeHeader() - WriteHeader()と同様に、wroteHeaderなら何もせずreturn func (rw *ResponseRecorder) writeHeader(b []byte, str string) { if rw.wroteHeader { return } if len(str) > 512 { str = str[:512] } m := rw.Header() _, hasType := m["Content-Type"] hasTE := m.Get("Transfer-Encoding") != "" if !hasType && !hasTE { if b == nil { b = []byte(str) } m.Set("Content-Type", http.DetectContentType(b)) } rw.WriteHeader(200) }

Slide 14

Slide 14 text

writeHeader() - 処理の最後にWriteHeader(200)を呼び出している func (rw *ResponseRecorder) writeHeader(b []byte, str string) { if rw.wroteHeader { return } if len(str) > 512 { str = str[:512] } m := rw.Header() _, hasType := m["Content-Type"] hasTE := m.Get("Transfer-Encoding") != "" if !hasType && !hasTE { if b == nil { b = []byte(str) } m.Set("Content-Type", http.DetectContentType(b)) } rw.WriteHeader(200) }

Slide 15

Slide 15 text

writeHeader() - つまりWrite()は(body書き込み前に)HTTPステータスコード200を設定する func (rw *ResponseRecorder) writeHeader(b []byte, str string) { if rw.wroteHeader { return } if len(str) > 512 { str = str[:512] } m := rw.Header() _, hasType := m["Content-Type"] hasTE := m.Get("Transfer-Encoding") != "" if !hasType && !hasTE { if b == nil { b = []byte(str) } m.Set("Content-Type", http.DetectContentType(b)) } rw.WriteHeader(200) }

Slide 16

Slide 16 text

まとめ - http.ResponseWriterの3つのメソッドは呼ぶ順番に意味がある - https://pkg.go.dev/net/http#ResponseWriter - 200以外のHTTPステータスコードを設定する場合、Writeメソッドの前に WriteHeaderメソッドを明示的に呼び出す必要がある - 実装例としてhttptest.ResponseRecorderを読んだ - 同一インスタンスで一度のみHTTPステータスコードが設定できる - Table Driven Testなどではインスタンスを使いまわさずにサブテスト内で初期化必須

Slide 17

Slide 17 text

fin