Upgrade to Pro — share decks privately, control downloads, hide ads and more …

HTTPステータスコードが意図した値にならないとき Let's Go Talk #2

Aa0368c69701536321af9db1ae552b45?s=47 aboy
August 03, 2022

HTTPステータスコードが意図した値にならないとき Let's Go Talk #2

Let's Go Talk #2でLTした資料です。

Aa0368c69701536321af9db1ae552b45?s=128

aboy

August 03, 2022
Tweet

More Decks by aboy

Other Decks in Programming

Transcript

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

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

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

  4. 例えばこんなコード 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)
  5. 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)
  6. 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から抜粋
  7. 実装例)httptest.ResponseRecorder - httptestパッケージのResponseRecorderを読んでいく - ResponseRecorderはhttp.ResponseWriterを実装したもの

  8. 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() }
  9. 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() }
  10. 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() }
  11. 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 }
  12. 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) }
  13. 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) }
  14. 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) }
  15. 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) }
  16. まとめ - http.ResponseWriterの3つのメソッドは呼ぶ順番に意味がある - https://pkg.go.dev/net/http#ResponseWriter - 200以外のHTTPステータスコードを設定する場合、Writeメソッドの前に WriteHeaderメソッドを明示的に呼び出す必要がある - 実装例としてhttptest.ResponseRecorderを読んだ

    - 同一インスタンスで一度のみHTTPステータスコードが設定できる - Table Driven Testなどではインスタンスを使いまわさずにサブテスト内で初期化必須
  17. fin