net/http.ClientにHookをかける

昨日のこと。jszwedko/go-circleci というパッケージを使用してCircleCI EnterprizeのAPIを叩くという処理を実装していたのですが、どうにもうまくいかない。正直に言ってこのパッケージはドキュメントがしっかりしている、という訳ではないし、エラーメッセージを見ても何がだめなのか(そもそも現在使用しているCircleCI Enterpriseで使用できるかもよくわかっていなかった)わからない。 しかしまぁ、自分でHTTP requestを作ったりしてあれやこれややるのもまぁ面倒であるので、なんとかデバッグしたいと思ったのですが、外部のパッケージをフォークして変更を加えてデバッグする・・・という様なことはもちろんやりたくないわけです。 このパッケージは*http.Client を指定できます。*http.Clientはインターフェースではなく構造体なので、別の実装に置き換えるということはできません。が、その実装はほぼほぼ後述するhttp.RoundTripperなため、http.RoundTripperをラップして、HTTP requestとHTTP responseをログに吐けばまぁ、何が問題かわかるだろう、と考えました。 そんなモノは誰かがすでに書いているだろう、というのはさておき、http.RoundTripperを実際にいじってみるということはやったことがなかったので、勉強がてらnasa9084/go-logtransport なるものを書きました。 書いていく途中で、考えたことなど、記録に残しておくのも良さそうと思ったため、本記事とします。 http.Clientとhttp.RoundTripper Go言語でHTTPのリクエストを発行するには基本として*http.Client というものを使用します。簡便のため、GET 、POST 、Head についてはパッケージグローバルの関数も用意されてはいるのですが、これらも内部的にはパッケージグローバルで宣言されたDefaultClient という*http.Client が使用されています。 *http.Clientはゼロ値で使用できるようにまとめられた構造体で、DefaultClient は*http.Client{}と宣言されています。 普段はこの*http.Clientを使用してHTTPの通信を行うわけですが、実は*http.Clientはそれほど多くの機能は持っていません。実際、持っているフィールドはたったの4つ(Go1.13時点)しかないのです。*http.Clientはリダイレクトやクッキーなどの一部の処理だけを受け持っていて、実際のHTTP通信のほとんどはフィールドとして保持しているhttp.RoudTripperが行います。 http.RoundTripperはインターフェースとして定義されていて、自由に差し替えをすることができます。特に指定していない場合は*http.Transportがデフォルトの実装として使用されます。 Goの他の標準パッケージの例に漏れず、http.RoundTripperは非常にシンプルなインターフェースで、次の様に定義されています。 1 2 3 type RoundTripper interface { RoundTrip(*Request) (*Response, error) } RoundTrip()がHTTP requestを受け取り、HTTP responseを返します。つまり、requestのログをとり、子RoundTripperのRoundTrip()を実行し、Responseのログをとってそのまま返す、という様なラッパーを書けば良さそうです。 1 2 3 4 5 6 7 8 9 func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { // Requestのログをとる resp, _ := t.Transport.RoundTrip(r) // responseのログをとる return resp, nil } 実際にrequestとresponseのログをとるには、net/http/httputilパッケージのDump系関数が使用できます。今回はクライアント側の実装なので、httputil.DumpRequestOutとhttputil.DumpResponseを使用します。 テスト 実装の詳細はそれほど難しい内容ではないのでさておき、テストをどう書くか、という話をしましょう。 HTTPに関連したテストを書くとき、Go言語ではnet/http/httptestを使用すると便利です。 テストを書くにあたり、最初は子RoundTripperをモックして、適当にResponseを返すモノをつくればよいか、と思ったのですが、いい感じにテスト用のResponseを作成するのは面倒そうでした。 そういえば今日、http.Transportにロガーを仕込むの書いてみて、テストを書くのにRequestとResponse生成したいな・・・って考えてhttptestにないか見て、なんでないんや!って一瞬おこだったけどhttptest.Server使えばええんや、とすぐに思い直したのでアレがそれでそんな感じでした(とりとめが無い — nasa9084@某某某某(0x1a) (@nasa9084) October 31, 2019 しかし素直にhttptest.Serverを使えば、クライアント側では一切テスト用に特殊な実装を使用することなく、普通にリクエストをしてレスポンスを受けることができます(httptest.Serverは日常的に使っているのに、なぜ忘れていたのか・・・)。 これを使用して、シュッと適当なハンドラをサーブしてことなきをえました。 ...

2019-11-01 · nasa9084

きちんとやるnet/http

皆さん、net/httpパッケージは使っていますか? Go言語の標準パッケージであるnet/httpはPythonなどの標準HTTPパッケージに比べ、人間にとっても取り扱いがしやすいため、そのまま使用している方が多いかと思います。 しかし、このnet/httpパッケージ、簡単に使えるように見えて結構落とし穴が多いのです。 1. Response Bodyはクローズする必要がある 次のコードを見てみましょう。 1 2 3 4 5 6 7 8 9 resp, err := http.Get("https://example.com/api") if err != nil { return nil, err } var t T if err := json.NewDecoder(resp.Body).Decode(&t); err != nil { return nil, err } return &t, nil クライアントライブラリなどでよく書きそうな処理ですね。何も問題ないと思いましたか? 公式ドキュメント を見てみましょう。 It is the caller’s responsibility to close Body. Bodyをクローズするのは関数を呼んだ人の責任、とあります。そうです。Response.Bodyは Close()しなければならないのです。ちゃんとクローズされていない場合、次のリクエストでkeepaliveコネクションの再利用がされず、パフォーマンスの悪化やコネクションリークを起こす可能性があります。 2. Response Bodyを最後まで読む Response Bodyをきちんとクローズするように修正したコードが次のようなコードです。 1 2 3 4 5 6 7 8 9 10 resp, err := http.Get("https://example.com/api") if err != nil { return nil, err } defer resp.Body.Close() var t T if err := json.NewDecoder(resp.Body).Decode(&t); err != nil { return nil, err } return &t, nil deferを使うことできちんとクローズできているはずです。 さて、問題はないでしょうか?いいえ、これだけだとまだkeepaliveコネクションの再利用がされない恐れがあります。 ...

2019-01-08 · nasa9084

sygを使用したgraceful shutdown serverパターン

github.com/nasa9084/syg を使用すると、手軽にシグナルとコールバック関数のマッピングを行うことができます1 これを使用し、SIGINTを受けてgraceful shutdownできるHTTPサーバを実装してみます。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package app import ( "context" "net/http" "os" "time" "github.com/nasa9084/syg" ) type Server struct { server *http.Server closed chan struct{} } func NewServer() *Server { http.HandleFunc("/", longlongHandler) return &Server{ server: &http.Server{ Addr: ":8080", }, closed: make(chan struct{}), } } func (s *Server) Run() error { // os.Interrupt = syscall.SIGINT cancel := syg.Listen(s.shutdown, os.Interrupt) defer cancel() err := s.server.ListenAndServe() <-s.closed return err } func (s *Server) shutdown(os.Signal) { s.Shutdown(context.Background()) close(s.closed) } func longlongHandler(w http.ResponseWriter, r *http.Request) { // なんか長い処理のつもり time.Sleep(10 * time.Second) w.Write([]byte("hello")) } mainからは以下の様に呼びます。 ...

2018-03-10 · nasa9084

Application Specific Context

元ネタは@lestrrat さんの「Abusing type aliases to augment context.Context 」。 golangを用いてHTTPサーバを作る場合、ルーティングを定義するのに以下の様な関数を用います。 1 http.HandleFunc(path string, handler func(w http.ResponseWriter, r *http.Request) もちろん、http.Handleを用いる場合もありますし、gorilla/mux などのライブラリを用いることもあると思います。 ここで重要なのは、func(w http.ResponseWriter, r *http.Request)という引数の方です。 多くの場合、アプリケーションのハンドラ内ではデータベースなどの外部アプリケーション・ミドルウェアを用いることになります。 しかし、golangのHTTPアプリケーションでは、ハンドラ関数の形式がfunc (w http.ResponseWriter, r *http.Request)と決まっています。引数の追加はできないため、引数以外の方法でDB接続情報などを渡す必要があります。 これまで、golangでWebアプリケーション開発を行う場合によく用いられていたデータベースコネクションの保持方法は、dbパッケージを作成し、そこにパッケージ変数として持つ方法かと思います。が、グローバルな変数はできるだけ持ちたくない ですよね。 そこで、Go 1.8から追加されたcontext を使うことができます。http.Request にはcontext.Contextが入っていて、Request.Context() でget、Request.WithContext() でsetできます。 context.Contextに値を持たせる方法で最初に思いつくのはContext.WithValue() を用いる方法ですが、これは値を取得する度にtype assertionをする必要があり、あまりよくありません 。 これを解消するため、自分で型を定義するのがよいでしょう。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package context // internal context subpackage import ( "context" "errors" ) type withSomethingContext struct { context.Context something *Something } func WithSomething(ctx context.Context, something *Something) context.Context { return &withSomethingContext{ Context: ctx, something: something, } } func Something(ctx context.Context) (*Something, error) { if sctx, ok := ctx.(*withSomethingContext); ok { if sctx.something != nil { return sctx.something, nil } } return nil, errors.New(`no asscosiated something`) } このように定義をすることで、毎回type assertionをする必要もなくなり、すっきりします。 ...

2017-11-21 · nasa9084

golang: net/httpでBASIC認証

golangでベーシック認証するのはどうしたら良いのかなー。って思ってたら、net/httpでhandlerに渡されるhttp.RequestにBasicAuth()というメソッドが生えてました。 これはBASIC認証用のユーザ名、パスワード、ヘッダ解析のフラグという値を返してくれます。 なので、 1 2 3 4 5 6 7 8 9 func handler(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok { return } if username == "hogehogeuser" && password == "fugafugapasswd" { // something } } とすることで認証することができます。簡単、簡単。 なお残念ながらダイジェスト認証はサポートされていない様子。

2017-05-02 · nasa9084