$30 off During Our Annual Pro Sale. View Details »

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

aboy
August 03, 2022

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

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

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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から抜粋

    View Slide

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

    View Slide

  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()
    }

    View Slide

  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()
    }

    View Slide

  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()
    }

    View Slide

  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
    }

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

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

    View Slide

  17. fin

    View Slide